了解Haskell中的Fix数据类型

时间:2017-08-28 09:55:36

标签: haskell types monads free-monad

在关于Haskell中的Free Monads的this article中,我们给出了一个由以下定义的Toy数据类型:

data Toy b next =
    Output b next
  | Bell next
  | Done

修复定义如下:

data Fix f = Fix (f (Fix f))

允许通过保留常用类型来嵌套Toy表达式:

Fix (Output 'A' (Fix Done))              :: Fix (Toy Char)
Fix (Bell (Fix (Output 'A' (Fix Done)))) :: Fix (Toy Char)

我理解固定点如何适用于常规函数,但是我没有看到这里的类型如何减少。编译器遵循哪些步骤来评估表达式的类型?

1 个答案:

答案 0 :(得分:15)

我会使用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的相似之处吗?与fixf作为参数传递给自身的方式相同,FixCons作为参数传递给自身。让我们一起检查fixFix的定义:

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是一个仿函数实例,它只是将所有OutputS收集到一个列表中,如下所示:

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而不是拥有自己的数据类型的强大功能:generality。