或者具体而言,为什么我们使用foldr来编码列表和迭代来编码数字?
对于长篇大论的介绍感到抱歉,但我真的不知道如何命名我想问的事情,所以我需要先给出一些说明。这很大程度上来自this C.A.McCann post,只是不太满足我的好奇心,而且我也会用等级n和无限懒惰的东西来处理这些问题。
将数据类型编码为函数的一种方法是创建“模式匹配”函数,该函数为每种情况接收一个参数,每个参数是一个函数,它接收与该构造函数对应的值以及返回相同结果类型的所有参数。 / p>
对于非递归类型
,这一切都符合预期--encoding data Bool = true | False
type Bool r = r -> r -> r
true :: Bool r
true = \ct cf -> ct
false :: Bool r
false = \ct cf -> cf
--encoding data Either a b = Left a | Right b
type Either a b r = (a -> r) -> (b -> r) -> r
left :: a -> Either a b r
left x = \cl cr -> cl x
right :: b -> Either a b r
right y = \cl cr -> cr y
然而,与模式匹配的很好的比喻与递归类型分解。我们可能想做类似
的事情--encoding data Nat = Z | S Nat
type RecNat r = r -> (RecNat -> r) -> r
zero = \cz cs -> cz
succ n = \cz cs -> cs n
-- encoding data List a = Nil | Cons a (List a)
type RecListType a r = r -> (a -> RecListType -> r) -> r
nil = \cnil ccons -> cnil
cons x xs = \cnil ccons -> ccons x xs
但我们不能在Haskell中编写那些递归类型定义!通常的解决方案是强制cons / succ案例的回调应用于所有级别的递归而不仅仅是第一个递归(即,编写折叠/迭代器)。在此版本中,我们使用返回类型r
,其中递归类型为:
--encoding data Nat = Z | S Nat
type Nat r = r -> (r -> r) -> r
zero = \cz cf -> cz
succ n = \cz cf -> cf (n cz cf)
-- encoding data List a = Nil | Cons a (List a)
type recListType a r = r -> (a -> r -> r) -> r
nil = \z f -> z
cons x xs = \z f -> f x (xs z f)
虽然这个版本有效,但它更难以定义一些功能。例如,如果您可以使用模式匹配,那么为列表编写“尾部”函数或为数字编写“前任”函数是微不足道的,但如果您需要使用折叠,则会变得棘手。
所以我真正的问题:
我们如何确保使用折叠的编码与假设的“模式匹配编码”一样强大?有没有办法通过模式匹配和机械方式进行任意函数定义将其转换为仅使用折叠的一个? (如果是这样,这也有助于制作棘手的定义,例如在折叠器中使用tail或foldl,因为它不那么神奇)
为什么Haskell类型系统不允许“模式匹配”编码所需的递归类型?。是否有理由仅允许通过data
定义的数据类型中的递归类型?模式匹配是直接使用递归代数数据类型的唯一方法吗?是否与类型推理算法有关?
答案 0 :(得分:6)
给出一些归纳数据类型
data Nat = Succ Nat | Zero
我们可以考虑如何在此数据上进行模式匹配
case n of
Succ n' -> f n'
Zero -> g
显而易见的是,Nat -> a
类型的每个函数都可以通过提供适当的f
和g
以及制作Nat
的唯一方法来定义(baring) bottom)正在使用两个构造函数之一。
编辑:暂时考虑f
。如果我们通过提供适当的foo :: Nat -> a
和f
来定义函数g
,以便f
递归调用foo
,那么我们可以将f
重新定义为f' n' (foo n')
f'
使a = (a',Nat)
不是递归的。如果类型f' (foo n)
而不是我们可以写foo n = h $ case n
Succ n' -> f (foo n)
Zero -> g
。所以,不失一般性
data NatDict a = NatDict {
onSucc :: a -> a,
onZero :: a
}
这是使我的帖子的其余部分有意义的表述:
因此,我们可以将case语句视为应用“析构函数字典”
h $ case n of
Succ n' -> onSucc (NatDict f g) n'
Zero -> onZero (NatDict f g)
现在我们之前的案例陈述可以成为
newtype NatBB = NatBB {cataNat :: forall a. NatDict a -> a}
鉴于此,我们可以推导出
fromBB :: NatBB -> Nat
fromBB n = cataNat n (NatDict Succ Zero)
然后我们可以定义两个函数
toBB :: Nat -> NatBB
toBB Zero = Nat $ \dict -> onZero dict
toBB (Succ n) = Nat $ \dict -> onSucc dict (cataNat (toBB n) dict)
和
newtype NatAsFold = NatByFold (forall a. (a -> a) -> a -> a)
我们可以证明这两个函数见证了同构(直到快速和失去推理),从而表明
NatBB
(与Nat
完全相同)与newtype RecListType a r = RecListType (r -> (a -> RecListType -> r) -> r)
我们可以使用与其他类型相同的结构,并通过证明基础类型与代数推理(和归纳)同构来证明所得到的函数类型是我们想要的。
关于你的第二个问题,Haskell的类型系统是基于iso-recursive而不是equi-recursive类型。这可能是因为理论和类型推断更易于使用iso-recursive类型,并且它们具有所有的功能,它们只是对程序员部分施加了更多的工作。我想声称你可以在没有任何开销的情况下获得你的iso-recursive类型
{{1}}
但显然GHC优化器有时会对这些产生扼要:(。
答案 1 :(得分:3)
Scott encoding上的维基百科页面有一些有用的见解。简短的版本是,您所指的是Church编码,您的“假设模式匹配编码”是Scott编码。两者都是明智的做事方式,但教会编码需要使用更轻型的机器(特别是,它不需要递归类型)。
证明两者是等价的证据使用了以下想法:
churchfold :: (a -> b -> b) -> b -> [a] -> b
churchfold _ z [] = z
churchfold f z (x:xs) = f x (churchfold f z xs)
scottfold :: (a -> [a] -> b) -> b -> [a] -> b
scottfold _ z [] = z
scottfold f _ (x:xs) = f x xs
scottFromChurch :: (a -> [a] -> b) -> b -> [a] -> b
scottFromChurch f z xs = fst (churchfold g (z, []) xs)
where
g x ~(_, xs) = (f x xs, x : xs)
这个想法是因为churchfold (:) []
是列表上的标识,我们可以使用一个Church折叠来生成它所给出的列表参数以及它应该产生的结果。然后在链x1 `f` (x2 `f` (... `f` xn) ... )
中,最外面的f
会收到一对(y, x2 : ... : xn : [])
(对于我们不关心的某些y
),因此返回f x1 (x2 : ... : xn : [])
。当然,它还必须返回x1 : ... : xn : []
,以便f
的任何其他应用程序也可以正常工作。
(这实际上与strong (or complete) induction的数学原理的证明有点相似,来自“弱”或通常的归纳原理。
顺便说一下,你的Bool r
类型对于真正的教会布尔有点太大了 - 例如(+) :: Bool Integer
,但(+)
并不是教会布尔值。如果您启用RankNTypes
,则可以使用更精确的类型:type Bool = forall r. r -> r -> r
。现在它被迫变成多态,所以真正只包含两个(忽略seq
和底部)居民 - \t _ -> t
和\_ f -> f
。类似的想法也适用于你的其他教会类型。