在Idris中,有一些神奇的机器可以自动为用户定义的类型创建(依赖)消除器。我想知道是否可以使用Haskell类型做一些事情(可能更少依赖)。例如,给定
data Foo a = No | Yes a | Perhaps (Foo a)
我想生成
foo :: b -> (a -> b) -> (b -> b) -> Foo a -> b
foo b _ _ No = b
foo _ f _ (Yes a) = f a
foo b f g (Perhaps c) = g (foo b f g x)
我对多变量函数和泛型非常弱,所以我可以使用一些帮助开始。
答案 0 :(得分:7)
以下是使用GHC Generics执行此操作的开始。添加一些代码以重新关联package
会使这更好。需要更多实例,这可能存在符合人体工程学的问题。
编辑:呸,我懒得回到数据系列,以获得我的类型平等派遣的注意力。这温和地改变了界面。我怀疑有足够trickery和/或使用内射类型系列,这可以在没有数据系列或重叠实例的情况下完成。
(:+:)
答案 1 :(得分:5)
正如Pigworker在问题评论中所说的那样,使用默认的Generic
表示会导致极大的丑陋,因为我们在类型中没有关于递归的先验信息,我们必须通过以下方式挖掘递归事件:手动检查类型是否相等。我想在这里介绍具有显式f-代数式递归的替代解决方案。为此,我们需要一个替代的通用Rep
。可悲的是,这意味着我们无法轻易进入GHC.Generics
,但我希望这仍然会有所启发。
在我的第一个解决方案中,我的目标是在当前的GHC功能中尽可能简单地进行演示。第二个解决方案是TypeApplication
- 基于GHC 8的重型解决方案,具有更复杂的类型。
像往常一样开始:
{-# language
TypeOperators, DataKinds, PolyKinds,
RankNTypes, EmptyCase, ScopedTypeVariables,
DeriveFunctor, StandaloneDeriving, GADTs,
TypeFamilies, FlexibleContexts, FlexibleInstances #-}
我的通用表示是产品总和的固定点。它略微扩展了generics-sop
的基本模型,它也是产品的总和,但不是函数,因此不适合递归算法。我认为SOP总体上比任意嵌套类型更好的实际表示;你可以找到扩展的论据,为什么在paper中就是这种情况。简而言之,SOP删除了不必要的嵌套信息,并允许我们将元数据与基本数据分开。
但在其他任何事情之前,我们应该决定泛型类型的代码。在vanilla GHC.Generics
中,没有明确定义的代码类型,因为sums,product等的类型构造函数构成了ad-hoc类型级语法,我们可以使用类型类对它们进行调度。我们更依赖于依赖类型泛型中的常规表示,并使用显式代码,解释和函数。我们的代码应该是实物:
[[Maybe *]]
外部列表对构造函数的总和进行编码,每个内部[Maybe *]
编码构造函数。 Just *
只是一个构造函数字段,而Nothing
表示递归字段。例如,[Int]
的代码为['[], [Just Int, Nothing]]
。
type Rep a = Fix (SOP (Code a))
class Generic a where
type Code a :: [[Maybe *]]
to :: a -> Rep a
from :: Rep a -> a
data NP (ts :: [Maybe *]) (k :: *) where
Nil :: NP '[] k
(:>) :: t -> NP ts k -> NP (Just t ': ts) k
Rec :: k -> NP ts k -> NP (Nothing ': ts) k
infixr 5 :>
data SOP (code :: [[Maybe *]]) (k :: *) where
Z :: NP ts k -> SOP (ts ': code) k
S :: SOP code k -> SOP (ts ': code) k
请注意,NP
具有不同的递归和非递归字段构造函数。这非常重要,因为我们希望代码明确地反映在类型索引中。换句话说,我们希望NP
也充当[Maybe *]
的单身人士(虽然我们仍然在*
中保持参数化,但有充分理由)。
我们在定义中使用k
参数为递归留下了一个漏洞。我们像往常一样设置递归,将Functor
个实例留给GHC:
deriving instance Functor (SOP code)
deriving instance Functor (NP code)
newtype Fix f = In {out :: f (Fix f)}
cata :: Functor f => (f a -> a) -> Fix f -> a
cata phi = go where go = phi . fmap go . out
我们有两个类型系列:
type family CurryNP (ts :: [Maybe *]) (r :: *) :: * where
CurryNP '[] r = r
CurryNP (Just t ': ts) r = t -> CurryNP ts r
CurryNP (Nothing ': ts) r = r -> CurryNP ts r
type family Alg (code :: [[Maybe *]]) (r :: *) :: * where
Alg '[] r = ()
Alg (ts ': tss) r = (CurryNP ts r, Alg tss r)
CurryNP ts r
使结果类型为NP ts
的{{1}}次,并且还会在递归事件中插入r
。
r
计算Alg code r
上的代数类型。它将各个构造函数的消除器组合在一起。这里我们使用普通的嵌套元组,但当然SOP code r
- s也足够了。我们也可以在这里重用HList
作为NP
,但我发现它过于笨拙。
剩下的就是实现这些功能:
HList
这里的关键点是我们必须将uncurryNP :: CurryNP ts a -> NP ts a -> a
uncurryNP f Nil = f
uncurryNP f (x :> xs) = uncurryNP (f x) xs
uncurryNP f (Rec k xs) = uncurryNP (f k) xs
algSOP :: Alg code a -> SOP code a -> a
algSOP fs (Z np) = uncurryNP (fst fs) np
algSOP fs (S sop) = algSOP (snd fs) sop
gcata :: Generic a => Alg (Code a) r -> a -> r
gcata f = cata (algSOP f) . to
中的咖喱消除器转换为适当的" Alg
代数,因为这是可以在SOP code a -> a
中直接使用的形式。
让我们定义一些糖和实例:
cata
示例:
(<:) :: a -> b -> (a, b)
(<:) = (,)
infixr 5 <:
instance Generic (Fix (SOP code)) where
type Code (Fix (SOP code)) = code
to = id
from = id
instance Generic [a] where
type Code [a] = ['[], [Just a, Nothing]]
to = foldr (\x xs -> In (S (Z (x :> Rec xs Nil)))) (In (Z Nil))
from = gcata ([] <: (:) <: ()) -- note the use of "Generic (Rep [a])"
然而,如果我们讨好并且不必使用> gcata (0 <: (+) <: ()) [0..10]
55
- s或元组来存储消除器,那就更好了。最方便的方法是使用与标准库折叠相同的参数顺序,例如HList
或foldr
。在这种情况下,maybe
的返回类型由类型系列给出,该类型系列根据类型的通用代码进行计算。
gcata
这type family CurryNP (ts :: [Maybe *]) (r :: *) :: * where
CurryNP '[] r = r
CurryNP (Just t ': ts) r = t -> CurryNP ts r
CurryNP (Nothing ': ts) r = r -> CurryNP ts r
type family Fold' code a r where
Fold' '[] a r = r
Fold' (ts ': tss) a r = CurryNP ts a -> Fold' tss a r
type Fold a r = Fold' (Code a) r (a -> r)
gcata :: forall a r. Generic a => Fold a r
高度(完全)含糊不清。我们需要明确的应用程序或gcata
,我选择了前者,导致GHC 8依赖。但是,一旦我们提供Proxy
类型,结果类型就会减少,我们可以轻松地进行评估:
a
我在> :t gcata @[_]
gcata @[_] :: Generic [t] => r -> (t -> r -> r) -> [t] -> r
> :t gcata @[_] 0
gcata @[_] 0 :: Num t1 => (t -> t1 -> t1) -> [t] -> t1
> gcata @[_] 0 (+) [0..10]
55
上面使用了部分类型签名。我们也可以为此创建一个简写:
[_]
可以用作gcata1 :: forall f a r. Generic (f a) => Fold (f a) r
gcata1 = gcata @(f a) @r
。
我不想在这里详细说明implementation of the above gcata
。它不比简单版本长,但gcata1 @[]
实现非常多毛(令人尴尬的是,它对我的延迟答案负责)。现在我无法解释它,因为我用Agda援助写了它,这需要大量的自动搜索和类型俄罗斯方块。
答案 2 :(得分:1)
正如在评论和其他答案中所说的那样,最好从可以访问递归位置的通用表示开始。
使用此类表示的一个库是multirec
(另一个是compdata
):
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GADTs, TypeFamilies, MultiParamTypeClasses, RankNTypes #-}
module FooFold where
import Generics.MultiRec.FoldAlgK
import Generics.MultiRec.TH
data Foo a = No | Yes a | Perhaps (Foo a)
data FooF :: * -> * -> * where
Foo :: FooF a (Foo a)
deriveAll ''FooF
foldFoo :: (r, (a -> r, r -> r)) -> Foo a -> r
foldFoo phi = fold (const phi) Foo
FoldAlgK
模块提供具有单个结果类型的折叠,并将代数类型计算为嵌套对。另外还要相对容易。该库提供了一些其他变体。