您要求“您缺少的一般模式”。让我自己尝试解释一下,尽管 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
到 ,Identity
而从 到 反过来IdFn . flip ($) . runId
。等价的出现是因为基本上没有人可以对 type 做任何事情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
. 看,Either x y -> z
同构于
newtype EitherFn x y = EitherFn { runEitherFn :: forall z. (x -> z) -> (y -> z) -> z }
从中我们得到了模式的基本思想:
z
取一个没有出现在表达式主体中的新类型变量。
- 对于数据类型的每个构造函数,创建一个函数类型,它将其所有类型参数作为参数,并返回一个
z
. 调用与构造函数对应的这些“处理程序”。所以我们curry to的处理程序(x, y)
是,而处理程序是和。如果没有参数,你可以只取一个值作为你的函数,而不是更麻烦的.(x, y) -> z
x -> y -> z
Left x | Right y
x -> z
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
那里(“斯科特”编码),或者我们可以先用我们给定的功能来减少它,在这种情况下,它变成了z
使用我们已经拥有的功能(“Church”编码)。现在,由于已经预先为我们执行了递归,因此 Church 编码仅对有限数据结构完全等效;Scott 编码可以处理无限列表等。也很难理解如何以 Church 形式对相互递归进行编码,而 Scott 形式通常更简单一些。
无论如何,Church 编码有点难想,但更神奇一点,因为我们可以用一厢情愿的想法来处理它:“假设这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
上是两者的产物。()
但我之所以提到这一点,是因为插入 a并翻转参数以发现这实际上forall z. z -> (() -> z -> z) -> z
是 list 类型并不难[()]
,其值由给出,加法length
由 给出++
,乘法由 给出>>
。
为了提高效率,您可以进行 Church 编码data PosNeg x = Neg x | Zero | Pos x
并使用 Church 编码(保持它是有限的!)[Bool]
来形成 Church 编码,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
. 对方有一个case dispatch,但也不算太差。
递归数据上的递归算法呢?这就是它变得神奇的地方:在我们到达树本身之前,Church 编码已经运行了我们在子树上的任何函数foldr
。runBL
例如,假设我们要模拟这个函数:
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
。请注意,这两个“参数”与此处的“输出”类型相同。使用 Church 编码,我们必须像已经成功一样进行编程——这是一种称为“一厢情愿”的学科。
自由单子的教会编码
Free monad 有范式
data Free f x = Pure x | Roll f (Free f x)
我们的 Church 编码程序说这变成:
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