状态 monad 是如何工作的? (无代码解释)

How does the state monad work? (without code explanation)

在过去的几个月里,我一直在这里和那里抽出一些空闲时间来阅读有关 Monad 的内容。自大学时代以来,我就没有使用过函数式语言。所以我真的不记得 Haskell,当然也不知道 Scalaz。

在尝试将这些食物与大量 Haskell 代码相关联的同时,学习洋葱、墨西哥卷饼和三明治是一段艰难的时期。幸运的是,我偶然发现了两篇关键的文章,它们让我大吃一惊:图片中的单子,以及另一个来自命令式编码背景的人,他简单地写了等同于:

a -> b becomes m[a] -> m[b] (functor)
a -> m[b] becomes m[a] -> m[b] (monad)
and applicative is just a "function with context"

连同描述 bind 目的的最简单的句子,许多类别的内函子爆炸式地消失并变得简单。

Identity、List、Maybe 和其他突然有了意义。在这些知识的推动下,我开始考虑使用 monadic 类型在 C++ 中尝试一些 TMP,看看结果如何。

很快我就想传播状态。我想我现在阅读的关于状态 monad 和 monad 转换器的文章比我开始阅读 monad 的文章还多,但对于我来说,我无法理解它们。许多键盘都用坏了我敢肯定,在这些主题上输入的所有单词都会回答像我这样的人。

但我请你再受苦一次

a -> s -> (b, s)

函数接受一个值和一些状态,return一个新值和(可能)修改后的状态。 Bind 显然需要将 return 值和状态传播到下一个函数中。

我的问题是:monad 的工作是因为它们强加了一个结构。坚持结构和遵守法律会带来欢乐时光和彩虹。但是 a -> s -> (b, s) 看起来不像单子 a -> m[b].

即使应用'a',它仍然是s -> (b, s)。不同之处在于后一个函数采用(monadic?)STATE 和 returns 带有值的状态。而常规形式是:取一个 VALUE 和 returns 一个 WRAPPED 值。

输入的参数以及 return 类型的形式都不同。然而,许多文章都说这显然看起来像一个单子。对我来说肯定不是。

如果我要放松,我被告知必须持有的观点:不是将 m[a] 绑定到 a -> m[b],而是采用对 monad 有意义的任何参数并应用它适用于任何有意义的功能签名......然后我可以看到它的工作。

在那种情况下,我可以简单地说 "oh this monad binds State[s] AND 'a' to a function like a -> State[s] -> (State[s], b)"。然后它可以期望一个元组 return 并将其解压缩到下一个函数的参数中。

但不知何故,我怀疑有一种方法可以使状态 monad 像所有其他 monad 一样工作 - 包括以某种方式期望形式 a -> m[b] (但它如何通过状态线程?)。我怀疑这是可以做到的,因为我相信编写 Monad Transformers 将依赖于这些函数签名(形式?)的一致性。

即使您没有时间做出大的回应,link 从程序员的迫切角度看一篇文章也是天赐之物。

(对于任何不正确的术语使用,我深表歉意 - 我也在沿途学习)

我认为正在讨论的 monad 实例 确实 保留了 State 结构并且元组在不应该的时候让你陷入循环。

如果我们退后一步,问 bind 应该有什么签名才能符合我们对 monad 的期望,我们会得到:

(>>=) :: State s a -> (a -> State s b) -> State s b
someState >>= someFuncOfA = ...

并且我将在单子值构造函数周围加上额外的括号,记住它是 State s 而不是 只是 State 因为具体类型不能制作了一个 Monad 实例,只有一个参数的类型构造函数:

(>>=) :: [State s] a -> (a -> [State s] b) -> [State s] b
someState >>= someFuncOfA = ...

其中 [State s] 将是我们来自所有 monad 东西的特殊 m

因为 State 的新类型是

newtype State s a = State s -> (a, s)

我们知道,无论值构造一个 State 意味着什么,值采用的参数是一个 函数 s -> (a, s).

所以回到我们不完整的绑定定义:

(>>=) :: [State s] a -> (a -> [State s] b) -> [State s] b
someState >>= someFuncOfA = ...

我们知道 ... 必须是 State $ (someNewFunc) 因为这就是 State 的价值构建方式。

第一个观察:元组与此关系不大。 函数 someNewFunc 只要它的类型匹配值构造 State 所需的类型,就会保持我们想要的单子结构。 那个函数恰好需要类型s -> (a, s)这一事实几乎只是一个实现选择。签名可能有很多不同的东西,因为有 not really a difference between tuples and value constructors,我相信你可以先创建一个特殊的 data 类型,其值构造函数为此目的充当二元组,并使签名似乎人为地更好。

第二个观察:State 没有导出的 'State' 值构造函数,至少不是来自 Control.Monad.State,所以你使用辅助函数

state :: (s -> (a, s)) -> State s a

它再次将 函数 作为其第一个参数,并在幕后进行值构建。

所以我们知道在绑定定义里面,我们不能写

someState >>= someFuncOfA = State $ (someFunc)

相反,它需要

someState >>= someFuncOfA = state $ (someFunc)

state $ (someFunc) 计算时 导致类型 State s a 因为 someFunc 被迫具有类型 (s -> (a,s)) 并且将在事实上使用 runState 来完成这个。

这可能不是一篇很好的文章。我试图将 FP Complete State Monad tutorial 的相关部分翻译成解决这个特定问题的内容。也许阅读会提供更好的描述。

通过重读 FPComplete,Mr.F 的答案和另一个关于状态 monad 的 SO 答案,我终于有了惊喜时刻。

真正让我困惑的是被困在思考价值。有了我已经熟悉的 monad,我有这种心理障碍,我一直试图将状态 monad 视为增加一个值 - 所以部分 Mr.F 是正确的,元组让我感到困惑而且不必要地如此.

我认为 state monad 有两点真正让它点击:

  • 它的工作不是添加信息(就像 Maybe monad 可能会添加一个 Bool),而是添加一个 ability
  • 了解其功能的最佳方式是查看其 return.

在函数式编程中处理状态的方式是将其作为参数之一和 return 的一部分进行线程处理。所以 state monad 是关于线程化状态的。换句话说,它将无法线程状态的东西放大为能够线程状态的东西。

如果有一个 'a' 类型的值,使它的状态通过它线程化的方法是将它变成一个函数:s -> s a

这就是状态 monad 的 return 所做的。例如,如果我有一个值为 5 的 Int,状态 monad 的 return 会给我一个函数:s -> s 5(现在值“5”可以让状态以某种方式贯穿其中)。

当然 bind 可以使用 a -> mb.

形式的函数

因此,由于 State 包装了一个函数,该函数采用状态值和 returns 状态值(以及函数的常规 return 值!),状态感知的预期形式函数是:a -> mb 我们已经知道 mb 是一个可以 'thread' 状态的函数。所以它的行为很像 a -> s -> sb.

结论是,任何函数 a -> b,或任何值 'a' 现在都可以被 State monad 放大,以获得让状态线程化的能力。现在它可以很容易地与状态感知功能链接(绑定)。

这解决了其他困扰我的问题:为什么示例 Haskell 代码(我没有完全理解)必须在最后有这个额外的 运行 步骤。

一旦一切都被放大到线程状态,绑定被调用,剩下的实际上是一个函数。它使用 bind 和 fmap 将所有 a、b、c 链接到最终 return s r(状态和结果)。但它仍然缺少一件事:实际的初始状态。

因此您需要通过提供初始状态'run'或'evaluate'计算结果。

非常感谢 FPComplete、F 先生和一个很棒的 SO 答案,link 我已经输了:)

彩虹和独角兽,直到我决定现在处理 Monad 变形金刚。