您询问“您缺少的一般模式”。让我尝试解释一下,尽管 Petr Pudlák 的回答也相当不错。正如 user3237465 所说,我们可以使用两种编码,Church 和 Scott,并且您使用的是 Scott 而不是 Church。这是一般评论。
编码如何工作
通过连续传递,我们可以描述任何类型的值x
通过类型的某些独特功能
data Identity x = Id { runId :: x }
{- ~ - equivalent to - ~ -}
newtype IdentityFn x = IdFn { runIdFn :: forall z. (x -> z) -> z }
这里的“forall”非常重要,它表示这种类型离开z
作为未指定的参数。双射是Id . ($ id) . runIdFn
来自IdentityFn
to Identity
while IdFn . flip ($) . runId
反之亦然。之所以出现这种等价,是因为人们基本上无法对该类型做任何事情forall z. z
,没有足够普遍的操作。我们可以等价地指出newtype UnitFn = UnitFn { runUnitFn :: forall z. z -> z }
只有一个元素,即UnitFn id
,这意味着它对应于单位类型data Unit = Unit
以类似的方式。
现在柯里化观察(x, y) -> z
同构于x -> y -> z
是连续传递的冰山一角,它允许我们用纯函数来表示数据结构,没有数据结构,因为显然类型Identity (x, y)
因此相当于forall z. (x -> y -> z) -> z
。因此,将两个项目“粘合”在一起与创建这种类型的值相同,只是使用纯函数作为“粘合”。
要看到这种等价性,我们只需处理其他两个属性。
第一个是 sum 类型构造函数,其形式为Either x y -> z
. See, Either x y -> z
同构于
newtype EitherFn x y = EitherFn { runEitherFn :: forall z. (x -> z) -> (y -> z) -> z }
从中我们得到了该模式的基本思想:
- 采用新的类型变量
z
没有出现在表达式正文中。
- 对于数据类型的每个构造函数,创建一个函数类型,该函数类型将其所有类型参数作为参数,并返回
z
。将这些“处理程序”称为与构造函数相对应的。所以处理程序(x, y)
is (x, y) -> z
我们咖喱到x -> y -> z
,以及处理程序Left x | Right y
are x -> z
and y -> z
。如果没有参数的话,直接取一个值即可z
作为你的功能而不是更麻烦的() -> z
.
- 将所有这些处理程序作为表达式的参数
forall z. Handler1 -> Handler2 -> ... -> HandlerN -> z
.
- 同构的一半基本上只是将构造函数作为所需的处理程序传递;构造函数上的其他模式匹配并应用相应的处理程序。
微妙的缺失的东西
同样,将这些规则应用于各种事物是很有趣的;例如,正如我上面提到的,如果您将其应用于data Unit = Unit
你发现任何单位类型都是恒等函数forall z. z -> z
,如果你将其应用到data Bool = False | True
你找到逻辑函数forall z. z -> z -> z
where false = const
while true = const id
。但如果你真的使用它,你会发现仍然缺少一些东西。提示:如果我们看一下
data List x = Nil | Cons x (List x)
我们看到该模式应该如下所示:
data ListFn x = ListFn { runListFn :: forall z. z -> (x -> ??? -> z) -> z }
对于一些???
。上述规则并没有规定那里会发生什么。
有两个不错的选择:要么我们使用newtype
尽其所能地ListFn x
那里(“Scott”编码),或者我们可以用我们已经给出的函数预先减少它,在这种情况下它就变成了z
使用我们已有的功能(“Church”编码)。现在,由于我们已经预先执行了递归,因此 Church 编码仅与以下内容完全等效finite数据结构; Scott 编码可以处理无限列表等。也很难理解如何以 Church 形式编码相互递归,而 Scott 形式通常更简单一些。
不管怎样,教会编码有点难以思考,但也更神奇,因为我们可以带着一厢情愿的想法来接近它:“假设这个z
已经是你想要实现的目标tail list
,然后将其与head list
以适当的方式。”而这种一厢情愿的想法正是人们难以理解的原因foldr
,因为该双射的一侧恰好是foldr
列表中的。
还有一些其他问题,例如“如果,例如Int
or Integer
,构造函数的数量是大还是无限?”。这个特定问题的答案是使用函数
data IntFn = IntFn { runIntFn :: forall z. (z -> z) -> z -> z }
你问这是什么?好吧,一个聪明的人(Church)已经发现这是一种将整数表示为组合重复的方法:
zero f x = x
one f x = f x
two f x = f (f x)
{- ~ - increment an `n` to `n + 1` - ~ -}
succ n f = f . n f
其实在这个账户上m . n
是两者的产物。但我提到这一点是因为插入一个并不太难()
并翻转争论发现这实际上是forall z. z -> (() -> z -> z) -> z
这是列表类型[()]
,其值由下式给出length
和添加由++
和乘法给出>>
.
为了提高效率,您可以进行 Church-encodedata PosNeg x = Neg x | Zero | Pos x
并使用 Church 编码(保持有限!)[Bool]
形成教会编码PosNeg [Bool]
其中每个[Bool]
隐含地以未声明的方式结束True
在最后的最高有效位,因此[Bool]
代表从+1到无穷大的数字。
扩展示例:BinLeaf / BL
另一个重要的例子,我们可能会想到二叉树,它将所有信息存储在叶子中,但也包含内部节点上的注释:data BinLeaf a x = Leaf x | Bin a (BinLeaf a x) (BinLeaf a x)
。按照 Church 编码的方法,我们这样做:
newtype BL a x = BL { runBL :: forall z. (x -> z) -> (a -> z -> z -> z) -> z}
现在代替Bin "Hello" (Leaf 3) (Bin "What's up?" (Leaf 4) (Leaf 5)
我们用小写字母构造实例:
BL $ \leaf bin -> bin "Hello" (leaf 3) (bin "What's up?" (leaf 4) (leaf 5)
因此,同构是非常简单的一种方式:binleafFromBL f = runBL f Leaf Bin
。对方有案件派遣,但还不算太糟糕。
递归数据上的递归算法怎么样?这就是它变得神奇的地方:foldr
and runBL
在我们到达树本身之前,Church 编码都已经运行了子树上的任何函数。假设我们想要模拟这个函数:
sumAnnotate :: (Num n) => BinLeaf a n -> BinLeaf (n, a) n
sumAnnotate (Leaf n) = Leaf n
sumAnnotate (Bin a x y) = Bin (getn x' + getn y', a) x' y'
where x' = sumAnnotate x
y' = sumAnnotate y
getn (Leaf n) = n
getn (Bin (n, _) _ _) = n
我们该做什么?
-- pseudo-constructors for BL a x.
makeLeaf :: x -> BL a x
makeLeaf x = BL $ \leaf _ -> leaf x
makeBin :: a -> BL a x -> BL a x -> BL a x
makeBin a l r = BL $ \leaf bin -> bin a (runBL l leaf bin) (runBL r leaf bin)
-- actual function
sumAnnotate' :: (Num n) => BL a n -> BL n n
sumAnnotate' f = runBL f makeLeaf (\a x y -> makeBin (getn x + getn y, a) x y) where
getn t = runBL t id (\n _ _ -> n)
我们传入一个函数\a x y -> ... :: (Num n) => a -> BL (n, a) n -> BL (n, a) n -> BL (n, a) n
。请注意,这两个“参数”与此处的“输出”类型相同。使用教会编码,我们必须像已经成功一样进行编程——一门叫做“一厢情愿”的学科。
自由单子的教会编码
自由单子有范式
data Free f x = Pure x | Roll f (Free f x)
我们的教会编码程序说这变成了:
newtype Fr f x = Fr {runFr :: forall z. (x -> z) -> (f z -> z) -> z}
你的职能
matchFree p _ (Pure x) = p x
matchFree _ f (Free x) = f x
变得简单
matchFree' p f fr = runFr fr p f