ChatGPT解决这个技术问题 Extra ChatGPT

什么是自由单子?

我已经看到术语 Free Monad 弹出 every now and then 有一段时间了,但似乎每个人都只是使用/讨论它们而没有解释它们是什么.那么:什么是自由单子? (我想说我熟悉 monad 和 Haskell 基础知识,但对范畴论只有非常粗略的了解。)

一个相当好的解释在这里haskellforall.com/2012/06/…
@Roger 就是那种把我带到这里的页面。对我来说,该示例为名为“Free”的类型定义了一个 monad 实例,仅此而已。

J
Joseph Sible-Reinstate Monica

这是一个更简单的答案:Monad 是在 monadic 上下文被 join :: m (m a) -> m a 折叠时“计算”的东西(回想一下,>>= 可以定义为 x >>= y = join (fmap y x))。这就是 Monads 通过连续的计算链携带上下文的方式:因为在系列中的每一点,来自前一个调用的上下文与下一个调用折叠。

一个自由的 monad 满足所有的 Monad 定律,但不做任何折叠(即计算)。它只是建立了一系列嵌套的上下文。创建这种自由单子值的用户负责对这些嵌套上下文执行某些操作,以便可以将此类组合的含义推迟到创建单子值之后。


你的段落对菲利普的帖子做了很好的补充。
我真的很喜欢这个答案。
free monad 可以替代 Monad 类型类吗?也就是说,我是否可以仅使用免费 monad 的 return 和 bind 编写程序,然后使用我喜欢的 Mwybe 或 List 或其他任何一个来加入结果,或者甚至生成一个绑定/连接函数调用序列的多个 monadic 视图。忽略底部和非终止,即。
这个答案有帮助,但我认为如果我没有在 NICTA 课程中遇到“加入”并阅读 haskellforall.com/2012/06/…,我会感到困惑。所以对我来说,理解的诀窍是阅读大量答案,直到它沉入其中。(NICTA 参考:github.com/NICTA/course/blob/master/src/Course/Bind.hs
这个答案是有史以来最好的
M
Matthias Braun

Edward Kmett 的回答显然很棒。但是,它有点技术性。这是一个可能更容易理解的解释。

自由单子只是将函子变成单子的一般方法。也就是说,给定任何函子 f Free f 是一个单子。这不会很有用,除非你得到一对功能

liftFree :: Functor f => f a -> Free f a
foldFree :: Functor f => (f r -> r) -> Free f r -> r

其中第一个让你“进入”你的 monad,第二个让你有一种“离开”它的方法。

更一般地说,如果 X 是带有一些额外东西 P 的 Y,那么“免费 X”是一种从 Y 到 X 而不获得任何额外东西的方式。

示例:幺半群 (X) 是具有额外结构 (P) 的集合 (Y),它基本上表示它具有运算(您可以考虑加法)和一些恒等式(例如零)。

所以

class Monoid m where
   mempty  :: m
   mappend :: m -> m -> m

现在,我们都知道列表

data [a] = [] | a : [a]

好吧,给定任何类型 t 我们知道 [t] 是一个幺半群

instance Monoid [t] where
  mempty   = []
  mappend = (++)

所以列表是集合上的“自由幺半群”(或在 Haskell 类型中)。

好的,所以免费的单子是相同的想法。我们取一个函子,并返回一个单子。事实上,由于 monad 可以被看作是内函子范畴中的 monoid,所以列表的定义

data [a] = [] | a : [a]

看起来很像自由单子的定义

data Free f a = Pure a | Roll (f (Free f a))

Monad 实例与列表的 Monoid 实例相似

--it needs to be a functor
instance Functor f => Functor (Free f) where
  fmap f (Pure a) = Pure (f a)
  fmap f (Roll x) = Roll (fmap (fmap f) x)

--this is the same thing as (++) basically
concatFree :: Functor f => Free f (Free f a) -> Free f a
concatFree (Pure x) = x
concatFree (Roll y) = Roll (fmap concatFree y)

instance Functor f => Monad (Free f) where
  return = Pure -- just like []
  x >>= f = concatFree (fmap f x)  --this is the standard concatMap definition of bind

现在,我们得到了两个操作

-- this is essentially the same as \x -> [x]
liftFree :: Functor f => f a -> Free f a
liftFree x = Roll (fmap Pure x)

-- this is essentially the same as folding a list
foldFree :: Functor f => (f r -> r) -> Free f r -> r
foldFree _ (Pure a) = a
foldFree f (Roll x) = f (fmap (foldFree f) x)

这可能是我见过的对“免费”最好的平易近人的解释。尤其是以“更一般地”开头的段落。
我认为将 Free f a = Pure a | Roll (f (Free f a)) 视为 Free f a = a + fa + ffa + ... 很有趣,即“f 应用于任意次数”。然后concatFree(即join)采用“f 应用任意次数到(f 应用任意次数到a)”并将两个嵌套应用程序折叠成一个。并且 >>= 采用“f 对 a 应用任意次数”和“如何从 a 到(b 与 f 应用任意次数)”,并且基本上将后者应用于前者内部的 a 并折叠嵌套。现在我自己明白了!
“这可能是一个更容易理解的解释。 […] 事实上,由于 monad 可以被视为 endo functors 类别中的幺半群,…”不过,我认为这是一个非常好的答案。
“monads 可以被视为 endo functors 类别中的幺半群”<3(您应该链接到 stackoverflow.com/a/3870310/1306877,因为每个 haskeller 都应该知道该引用!)
@jkff:这个公式几乎是正确的!但是如果你仔细展开 Free 的递归定义,你不会得到 Free f a = a + f a + f f a + ...;你得到Free f a = a + f (a + f (a + ...) )。这些是不同的,因为 f (a + b)f a + f b 不同(不一定)。例如,取 []。您的公式会说 Free [] Int 由 Ints、Ints 列表、Int 列表列表等组成。但是 Free [] Int 更通用 - 它具有 [1, [2, 3], 4] 之类的内容,即不是这些。这源于 [a + b] 大于 [a] + [b] 的事实。
C
Community

免费的 foo 恰好是满足所有“foo”法则的最简单的东西。也就是说,它完全满足成为 foo 所必需的法律,仅此而已。

健忘函子是在结构从一个类别转移到另一个类别时“忘记”部分结构的函子。

给定函子 F : D -> CG : C -> D,我们说 F -| GF 左邻接 G,或者 G 右邻接 F,只要 forall a, b: F a -> b 是同构的到 a -> G b,其中箭头来自相应的类别。

形式上,一个自由函子伴随着一个健忘函子。

自由的 Monoid

让我们从一个更简单的例子开始,自由幺半群。

以一个由某个载体集 T 定义的幺半群、一个将一对元素混合在一起的二元函数 f :: T → T → T 和一个 unit :: T,这样你就有一个结合律和一个恒等律:{4 }。

您可以从幺半群的类别中创建一个函子 U(其中箭头是幺半群同态,也就是说,它们确保它们将 unit 映射到另一个幺半群上的 unit,并且您可以在映射到其他幺半群而不改变含义)到集合的类别(其中箭头只是函数箭头),它“忘记”了操作和unit,只给你载体集合。

然后,您可以定义一个函子 F,从集合类别回到与该函子相伴的幺半群类别。该函子是将集合 a 映射到幺半群 [a] 的函子,其中 unit = []mappend = (++)

因此,回顾一下到目前为止的示例,在伪 Haskell 中:

U : Mon → Set -- is our forgetful functor
U (a,mappend,mempty) = a

F : Set → Mon -- is our free functor
F a = ([a],(++),[])

那么为了证明 F 是自由的,我们需要证明它与 U 是一个遗忘函子,也就是说,正如我们上面提到的,我们需要证明

F a → ba → U b 同构

现在,请记住 F 的目标在幺半群的类别 Mon 中,其中箭头是幺半群同态,所以我们需要 a 来证明来自 [a] → b 的幺半群同态可以由来自 a → b 的函数精确描述.

在 Haskell 中,我们称这部分存在于 Set(呃,Hask,我们假装为 Set 的 Haskell 类型的类别),只是 foldMap,当从 Data.Foldable 专门化为 Lists 时,它具有类型Monoid m => (a → m) → [a] → m

这是一个附加的后果。值得注意的是,如果你忘记了然后用 free 建立,然后又忘记了,就像你忘记了一次一样,我们可以用它来建立 monadic join。由于 UFUF ~ U(FUF) ~ UF,我们可以通过定义我们的附加的同构,将恒等幺半群同态从 [a] 传递到 [a],得到来自 [a] → [a] 的列表同构是一个函数a -> [a] 类型的,这只是列表的返回。

您可以通过以下术语描述列表来更直接地组合所有这些:

newtype List a = List (forall b. Monoid b => (a -> b) -> b)

自由单子

那么什么是自由单子呢?

好吧,我们做和之前一样的事情,我们从一个遗忘函子 U 开始,从箭头是单子同态的单子类别到箭头是自然变换的内函子类别,我们寻找一个左伴随的函子到那个。

那么,这与通常使用的自由单子的概念有什么关系呢?

知道某物是自由单子 Free f,告诉您从 Free f -> m 给出单子同态与从 f -> m 给出自然变换(函子同态)是同一件事(同构)。记住 F a -> b 必须与 a -> U b 同构,F 才能与 U 相邻。U 在这里将单子映射到函子。

F 至少与我在 hackage 的 free 包中使用的 Free 类型同构。

我们也可以更紧密地类比上面的自由列表代码,通过定义

class Algebra f x where
  phi :: f x -> x

newtype Free f a = Free (forall x. Algebra f x => (a -> x) -> x)

Cofree Comonads

假设它存在,我们可以通过查看一个健忘函子的右伴随物来构造类似的东西。 cofree 函子与遗忘函子简单地/右伴随/,并且通过对称性,知道某物是 cofree 共单子与知道从 w -> Cofree f 给出共单子同态与从 {2 给出自然变换是一样的}。


@PauloScardine,这不是您需要担心的。我的问题来自于对了解一些高级数据结构的兴趣,并且可能对目前 Haskell 开发的前沿有所了解——这绝不是必要的,也没有代表到目前为止实际编写 Haskell 的内容。 (请注意,一旦您再次通过 IO 学习阶段,它会变得更好)
@PauloScardine 即使使用免费的单子,您也不需要上述答案即可在 Haskell 中高效地编程。事实上,我不建议没有范畴论背景的人以这种方式攻击自由单子。有很多方法可以从操作的角度来讨论它,并在不深入范畴论的情况下了解如何使用一种方法。但是,如果不深入理论,我就不可能回答关于它们来自哪里的问题。自由结构是范畴论中的强大工具,但您不需要这些背景即可使用它们。
@PauloScardine:您完全不需要微积分就可以有效地利用 Haskell,甚至了解您在做什么。当数学只是你可以用来娱乐和获利的额外好处时,抱怨“这种语言是数学的”有点奇怪。你不会在大多数命令式语言中得到这些东西。你为什么要抱怨额外的东西?您可以选择不进行数学推理,并像对待任何其他新语言一样处理它。
@Sarah:我还没有看到一篇关于haskell 的文档或IRC 对话,它对计算机理论和lambda 演算therms 并不重。
@PauloScardine 这有点过时了,但在 Haskell 的辩护中:类似的技术事物适用于所有其他编程语言,只是 Haskell 有如此好的编译,人们实际上可以喜欢谈论它们。为什么/如何 X 是一个单子对很多人来说很有趣,关于 IEEE 浮点标准的讨论不是;这两种情况对大多数人来说都无关紧要,因为他们可以使用结果。
c
comonad

Free Monad(数据结构)之于 Monad(类)就像 List(数据结构)之于 Monoid(类):这是一个简单的实现,您可以在之后决定如何组合内容。

您可能知道 Monad 是什么,并且每个 Monad 都需要 fmap + join + returnbind + return 的特定(遵守 Monad 法则)实现。

让我们假设您有一个 Functor(fmap 的实现),但其余部分取决于运行时所做的值和选择,这意味着您希望能够使用 Monad 属性但希望选择 Monad 函数然后。

这可以使用 Free Monad(数据结构)来完成,它以这样的方式包装 Functor(类型),以便 join 是这些仿函数的堆叠而不是归约。

您要使用的实际 returnjoin 现在可以作为归约函数 foldFree 的参数给出:

foldFree :: Functor f => (a -> b) -> (f b -> b) -> Free f a -> b
foldFree return join :: Monad m => Free m a -> m a

为了解释这些类型,我们可以将 Functor f 替换为 Monad m 并将 b 替换为 (m a)

foldFree :: Monad m => (a -> (m a)) -> (m (m a) -> (m a)) -> Free m a -> (m a)

这个答案给我的印象是我了解它们甚至可能有用的地方。
G
Gabriella Gonzalez

Haskell free monad 是一个函子列表。相比:

data List a   = Nil    | Cons  a (List a  )

data Free f r = Pure r | Free (f (Free f r))

Pure 类似于 NilFree 类似于 Cons。一个自由的 monad 存储一个仿函数列表而不是一个值列表。从技术上讲,您可以使用不同的数据类型实现自由 monad,但任何实现都应该与上述实现同构。

每当您需要抽象语法树时,您都可以使用免费的 monad。自由单子的基本函子是语法树每一步的形状。

My post,有人已经链接了,给出了几个如何使用免费 monad 构建抽象语法树的示例


我知道你只是在做一个类比而不是下一个定义,但是一个自由的 monad 在任何意义上肯定都不类似于函子列表。它更接近于一棵函子树。
我坚持我的术语。例如,使用我的 index-core 包,您可以定义“free monad comprehensions”,它的行为就像 list monad,只是您绑定的是函子而不是值。自由单子是函子的列表,如果您将所有 Haskell 概念转换为函子类别,那么列表将成为自由单子。然后,真正的函子树就变成了完全不同的东西。
你说得对,从某种意义上说,monad 是幺半群概念的分类,因此自由单子类似于自由幺半群,即列表。就这点而言,你当然是正确的。但是,自由 monad 的值的结构不是列表。它是一棵树,如 I detail below
@TomEllis 从技术上讲,如果您的基本函子是产品函子,它只是一棵树。当您将 sum 函子作为基本函子时,它更类似于堆栈机。
T
Tom Ellis

我认为一个简单的具体例子会有所帮助。假设我们有一个函子

data F a = One a | Two a a | Two' a a | Three Int a a a

带有明显的 fmap。那么 Free F a 是叶子的类型为 a 并且其节点用 OneTwoTwo'Three 标记的树的类型。 One-节点有一个子节点,Two- 和 Two'- 节点有两个子节点,Three-节点有三个并且还用 Int 标记。

Free F 是一个单子。 returnx 映射到只是具有值 x 的叶子的树。 t >>= f 查看每一片叶子并用树木代替它们。当叶子的值为 y 时,它将用树 f y 替换该叶子。

图表使这一点更清楚,但我没有轻松绘制图表的设施!


你们所说的是自由单子采用函子本身的形状。所以如果函子是树状的(产品),自由单子是树状的;如果它是类列表(总和),则免费 monad 是类列表;如果它是函数式的,那么 free monad 就是函数式的;等等。这对我来说很有意义。所以就像在一个自由的幺半群中一样,你不断地将 mappend 的每个应用程序视为创建一个全新的元素;在 free monad 中,您将 functor 的每个应用程序都视为一个全新的元素。
即使函子是“和函子”,得到的自由单子仍然是树状的。您最终会在树中得到不止一种类型的节点:总和的每个组成部分都有一个。如果你的“和函子”是 X -> 1 + X 那么你确实得到了一个列表,它只是一种退化的树。
C
CR Drost

试图在此处的超简单答案和完整答案之间提供“桥梁”答案。

所以“自由单子”从任何“函子”中构建了一个“单子”,所以让我们按顺序来看看。

函子详解

有些东西是类型级别的形容词,这意味着它们采用诸如“整数”之类的类型名词并给您返回不同的类型名词,例如“整数列表”或“带有整数的字符串对”,甚至是“用整数创建字符串的函数”整数。”为了表示任意形容词,让我使用替代词“蓝色”。

但随后我们注意到其中一些形容词在它们修饰的名词中是输入式或输出式的模式。例如,“用 __ 制作字符串的函数”是输入式的,“将字符串变成 __ 的函数”是输出式的。这里的规则涉及我有一个函数 X → Y,一些形容词“蓝色”是输出的,或者一个函子,如果我可以使用这样的函数将蓝色 X 转换为蓝色 Y。想想“消防软管喷洒 Xs”和然后你拧上这个 X → Y 函数,现在你的消防水带喷 Ys。或者如果它是相反的,它是输入或逆变的,真空吸尘器吸着 Ys,当我把它拧上时,我得到一个吸尘器吸着 Xs。有些东西既不是输出也不是输入。事实证明两者都是幻影:它们与它们所描述的名词完全无关,因为您可以定义一个函数“coerce”,它接受一个蓝色 X 并生成一个蓝色 Y,但 * 不知道X 或 Y 类型的详细信息”,甚至不需要它们之间的函数。

所以“__ 的列表”是输出的,你可以将 X → Y 映射到 X 的列表上以获得 Y 的列表。类似地,“一对字符串和一个__”是输出的。同时,“从 __ 到自身的函数”既不是输出也不是输入,而“一个字符串和一个正好为零的 __s 列表”可能是“幻像”情况。

但是,是的,这就是编程中的函子的全部内容,它们只是可映射的东西。如果某物是函子,那么它是唯一的函子,最多只有一种方法可以将函数映射到数据结构上。

单子

一个 monad 是一个函子,它同时是

普遍适用,给定任何 X,我可以构造一个蓝色 X,可以重复而不会改变太多含义。所以从某种意义上说,蓝蓝 X 与蓝 X 是一样的。

所以这意味着有一个规范函数将任何蓝蓝 __ 折叠成一个蓝色 __。 (而且我们当然会添加法律以使一切正常:如果其中一层蓝色来自通用应用程序,那么我们只想删除该通用应用程序;此外,如果我们将蓝-蓝-蓝 X 扁平化为blue X,无论我们先折叠前两个蓝色还是先折叠后两个蓝色,都应该没有区别。)

第一个例子是“一个可以为空的__”。因此,如果我给你一个可为空的可为空的 int,从某种意义上说,我给你的不仅仅是一个可为空的 int。或者“一个整数列表的列表”,如果要点只是包含 0 个或多个整数,那么“一个整数列表”就可以正常工作,正确的折叠是将所有列表连接到一个超级列表中。

Monads 在 Haskell 中变得很大,因为 Haskell 需要一种在现实世界中做事的方法,而不会违反它的数学纯世界,那里什么都没有发生。解决方案是添加一种淡化的元编程形式,我们在其中引入了“产生__的程序”的形容词。那么如何获取当前日期呢?好吧,Haskell 不能在没有 unsafePerformIO 的情况下直接执行此操作,但它可以让您描述和编写生成当前日期的程序。在这个愿景中,您应该描述一个不产生任何内容的程序,称为“Main.main”,并且编译器应该接受您的描述并将该程序作为二进制可执行文件交给您,以便您随时运行。

无论如何,“一个产生一个产生一个 int 的程序的程序”并没有比“一个产生一个 int 的程序”买多少,所以这结果是一个 monad。

免费单子

与函子不同,单子不是唯一的单子。给定函子不仅有一个 monad 实例。例如“一对 int 和一个 __”,你用这个 int 做什么?你可以加它,你可以乘它。如果将其设为可为空的 int,则可以保留最小的非空值或最大的非空值,或者最左边的非空值或最右边的非空值。

给定函子的自由单子是“最无聊”的构造,它只是“对于任何 n = 0, 1, 2, ...,自由蓝色 X 是蓝色 X”。

它是通用的,因为 blue⁰ X 只是一个 X。而一个 free blue free blue X 是一个 bluem bluen X,它只是一个 bluem+n X。它实现了“collapse”,因此根本不实现 collapse,在内部,blues 是任意嵌套。

这也意味着您可以将您选择的 monad 推迟到以后的日期,稍后您可以定义一个函数,将蓝蓝 X 简化为蓝色 X 并将所有这些折叠为 blue0,1 X,然后从 X 中提取另一个函数to blue X 始终为您提供 blue1 X。


非常感谢您如此接地气的解释!我无需了解 Haskell(我是 scala FP 爱好者)就能够得到这个非常简单的想法。所以现在我更清楚了,这听起来像是 Coyoneda 技巧(惰性函子)的延续,但我们也推迟了 flatMap 部分。使用 Coyoneda,我们只是封装了所有函数序列,直到我们知道如何应用它们,在 Free Monads 中,我们通过将纯值和挂起封装在数据结构中来制作一种“惰性 monad”,因此我们可以稍后提供一个实际的 monad .
这是一个很棒的答案!

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅