【问题标题】:Understanding the Fix datatype in Haskell了解 Haskell 中的 Fix 数据类型
【发布时间】:2018-02-05 13:32:13
【问题描述】:

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

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

Fix 定义如下:

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)

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

【问题讨论】:

  • 尝试将其与普通递归类型定义 data ToyR b = OutputR b (ToyR b) | BellR (ToyR b) | DoneR 进行比较。您会发现它们具有相同的值,只是Fix 变体在每个Toy 构造函数之后都有一个额外的Fix 构造函数。这是将类型从 Toy b (Fix (Toy b)) 折叠到 Fix (Toy b) 所必需的。

标签: haskell monads free-monad fixpoint-combinators


【解决方案1】:

我将使用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 是一个函子实例,它简单地将所有 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 而不是拥有自己的数据类型的强大之处:通用性。

【讨论】:

  • 如果我只有一个 Fix (Cons a) 和一个函数 (getLength) Cons a int -> Int,那么如何创建 Cons a Int 类型的对象?我可以从其他任何东西获得Int 的唯一方法是使用getLength 函数,但为此,我已经需要一个Cons a Int,而我没有(我只有一个Fix (Cons a))。
  • 我知道,如果我有任何类型的Nil,我可以产生一个Int 0,所以特别是我可以采用Fix (Cons a) 并产生一个Int,如果值恰好是Nil。但是,getLength 仅为Cons a Int 类型的输入定义,即使Nil 恰好也是该类型的有效值。
  • 没关系,我通过研究这篇文章找到了答案:medium.com/@olxc/catamorphisms-and-f-algebras-b4e91380d134。本质上,我忘记了fmap。给定我们的length 函数(Fix (Cons a) -> Int),它构造了一个类型为Cons (Fix (Cons a)) -> Cons a Int 的提升函数,所以魔法发生在那里,其中Fix (Cons a) 类型的Nil 被“映射”到类型为Nil Cons a Int 基本上什么都不做。 :)
  • unwrap 的定义类型不正确;您需要先将 Fix 的访问器方法应用于 n 。见这里:wiki.haskell.org/Catamorphisms
猜你喜欢
  • 2020-05-14
  • 1970-01-01
  • 2016-06-01
  • 2021-03-22
  • 2017-08-13
  • 1970-01-01
  • 1970-01-01
  • 2014-04-15
相关资源
最近更新 更多