在 Ed Kmett 的recursion-scheme
包中,有三个声明:
newtype Fix f = Fix (f (Fix f))
newtype Mu f = Mu (forall a. (f a -> a) -> a)
data Nu f where
Nu :: (a -> f a) -> a -> Nu f
这三种数据类型有什么区别?
在 Ed Kmett 的recursion-scheme
包中,有三个声明:
newtype Fix f = Fix (f (Fix f))
newtype Mu f = Mu (forall a. (f a -> a) -> a)
data Nu f where
Nu :: (a -> f a) -> a -> Nu f
这三种数据类型有什么区别?
Mu
将递归类型表示为其折叠,并将Nu
其表示为展开。在 Haskell 中,这些是同构的,并且是表示同一类型的不同方式。如果您假设 Haskell 没有任意递归,那么这些类型之间的区别会变得更有趣:Mu f
是 的最小(初始)不动点f
,并且Nu f
是其最大(终端)不动点。
的不动点是和之间的同构f
类型,即一对反函数, 。该类型只是使用 Haskell 的内置类型递归来直接声明同构。但是您可以同时为和实现输入/输出。T
T
f T
in :: f T -> T
out :: T -> f T
Fix
Mu
Nu
举一个具体的例子,假装你不能写递归值。的居民Mu Maybe
,即值:: forall r. (Maybe r -> r) -> r
,是自然人,{0, 1, 2, ...};的居民Nu Maybe
,即值:: exists x. (x, x -> Maybe x)
,是自然数 {0, 1, 2, ..., ∞}。想想这些类型的可能值,看看为什么Nu Maybe
有一个额外的居民。
如果您想对这些类型有一些直觉,那么在没有递归的情况下实现以下内容可能是一个有趣的练习(大致按难度递增的顺序):
zeroMu :: Mu Maybe
,succMu :: Mu Maybe -> Mu Maybe
zeroNu :: Nu Maybe
, succNu :: Nu Maybe -> Nu Maybe
,inftyNu :: Nu Maybe
muTofix :: Mu f -> Fix f
,fixToNu :: Fix f -> Nu f
inMu :: f (Mu f) -> Mu f
,outMu :: Mu f -> f (Mu f)
inNu :: f (Nu f) -> Nu f
,outNu :: Nu f -> f (Nu f)
您也可以尝试实现这些,但它们需要递归:
nuToFix :: Nu f -> Fix f
,fixToMu :: Fix f -> Mu f
Mu f
是最小不动点,Nu f
也是最大的,所以写一个函数:: Mu f -> Nu f
很容易,但写一个函数:: Nu f -> Mu f
很难;这就像逆流而上。
(有一次我打算对这些类型写一个更详细的解释,但对于这种格式来说可能有点太长了。)