我将使用Fix 制作一个更熟悉、更简单的类型,看看你是否能理解。
下面是普通递归定义中的列表类型:
data List a = Nil | Cons a (List a)
现在,回想一下我们如何将fix 用于函数,我们知道我们必须将函数作为参数传递给自身。事实上,由于List 是递归的,我们可以编写一个更简单的非递归数据类型,如下所示:
data Cons a recur = Nil | Cons a recur
你能看出这与函数f a recur = 1 + recur a 有何相似之处吗?与fix 将f 作为参数传递给自身的方式相同,Fix 将Cons 作为参数传递给自身。让我们并排检查fix 和Fix 的定义:
fix :: (p -> p) -> p
fix f = f (fix f)
-- Fix :: (* -> *) -> *
newtype Fix f = Fix {nextFix :: f (Fix f)}
如果你忽略构造函数名称的绒毛等,你会发现它们本质上是完全相同的定义!
对于 Toy 数据类型的示例,可以像这样递归地定义它:
data Toy a = Output a (Toy a) | Bell (Toy a) | Done
但是,我们可以使用Fix 将自身传递给自身,将Toy a 的所有实例替换为第二个类型参数:
data ToyStep a recur = OutputS a recur | BellS recur | DoneS
所以,我们可以只使用Fix (ToyStep a),它等同于Toy a,尽管形式不同。事实上,让我们证明它们是等价的:
toyToStep :: Toy a -> Fix (ToyStep a)
toyToStep (Output a next) = Fix (OutputS a (toyToStep next))
toyToStep (Bell next) = Fix (BellS (toyToStep next))
toyToStep Done = Fix DoneS
stepToToy :: Fix (ToyStep a) -> Toy a
stepToToy (Fix (OutputS a next)) = Output a (stepToToy next)
stepToToy (Fix (BellS next)) = Bell (stepToToy next)
stepToToy (Fix (DoneS)) = DoneS
您可能想知道,“为什么要这样做?”通常,没有太多理由这样做。但是,定义这些类型的简化版本的数据类型实际上可以让您制作极具表现力的函数。这是一个例子:
unwrap :: Functor f => (f k -> k) -> Fix f -> k
unwrap f n = f (fmap (unwrap f) n)
这真是一个不可思议的功能!当我第一次看到它时,我很惊讶!下面是一个使用我们之前创建的 Cons 数据类型的示例,假设我们创建了一个 Functor 实例:
getLength :: Cons a Int -> Int
getLength Nil = 0
getLength (Cons _ len) = len + 1
length :: Fix (Cons a) -> Int
length = unwrap getLength
这本质上是免费的fix,因为我们在我们使用的任何数据类型上都使用Fix!
现在让我们想象一个函数,假设 ToyStep a 是一个函子实例,它简单地将所有 OutputSs 收集到一个列表中,如下所示:
getOutputs :: ToyStep a [a] -> [a]
getOutputs (OutputS a as) = a : as
getOutputs (BellS as) = as
getOutputs DoneS = []
outputs :: Fix (ToyStep a) -> [a]
outputs = unwrap getOutputs
这就是使用Fix 而不是拥有自己的数据类型的强大之处:通用性。