“Learn you a Haskell”中关于State Monad代码的困惑

23

我正在使用在线书籍"Learn you a Haskell for great Good"学习Haskell。

据我所知,直到我遇到介绍State Monad的章节为止,我已经能够理解单子了。

然而,所呈现的代码声称是State类型的Monad实现(我无法在Hoogle中找到它),对我来说似乎太难以处理了。

  • 首先,我不明白它背后的逻辑,即为什么它应该起作用,作者是如何考虑这种技术的。(也许可以推荐相关文章或白皮书?)

  • 在第4行,建议函数f接受1个参数。
    然而,在几行之后,我们看到pop,它不带任何参数!

  • 扩展点1,作者试图使用函数表示状态是为了什么。

非常感谢任何帮助我理解发生了什么。

编辑

致有关人员:

下面的答案全面回答了我的问题。
我想补充一点:

在阅读下面推荐的文章后,我找到了上述第二个问题的答案: 所有这些时间,我"假设"pop函数将像这样使用:
stuff >>= pop,因为在绑定类型中,第二个参数是函数,而正确的用法是这样的pop >>= stuff,我在再次阅读如何将do-notation转换为普通绑定lambda之后意识到了这一点。


3
它并不声称是状态类型的单子实现,只是等效于它。实际编译器更高效地实现状态单子(这就是为什么没有可访问的构造函数),但更为复杂。 - leftaroundabout
@leftaroundabout 我的意思只是它可能不是真正的,只是为了教育目的而存在,就像你提到的那样。 - byrondrossos
2
这是真正的源代码(警告:Monad变换器)。 - Dan Burton
10
@DanBurton 在 Haskell 中有很多令人激动的地方,你应该把“_warning:_”改成“_spoiler:_”。 - byrondrossos
1
推荐阅读单子模型相关文章:"You could have invented monads...",链接为http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html - John L
4个回答

19

State单子代表有状态的计算,即使用来自某个外部状态的值并且可能修改该状态的计算。当您将有状态的计算顺序组合在一起时,后续的计算可能会根据先前的计算如何修改状态而给出不同的结果。

由于Haskell中的函数必须是纯的(即没有副作用),我们通过要求每个函数都接受一个表示当前世界状态的附加参数并返回一个表示修改状态的附加值来模拟外部状态的影响。实际上,外部状态被穿过一系列计算,就像我刚才在MSPaint中画的这个可怕的图表一样:

enter image description here

注意每个方框(代表一个计算)都有一个输入和两个输出。
如果您查看StateMonad实例,您会发现(>>=)的定义告诉您如何进行这种线程处理。它说要将一个有状态的计算绑定到一个接受有状态计算结果并返回另一个有状态计算的函数中,我们需要执行以下操作:
1. 使用初始状态运行以获取结果和新状态:(val, s1) 2. 将val提供给函数f以获取一个新的有状态计算 3. 使用修改后的状态运行新的计算
这个对于已经带有n个参数的函数如何运作?因为Haskell中每个函数默认都是柯里化的,所以我们只需要在末尾添加一个额外的参数(用于状态),并且函数现在返回一个二元组,其第二个元素是新修改的状态,而不是正常的返回值。因此,代替原来的返回值,现在我们会得到一个二元组。
f :: a -> b

我们现在有。
f :: a -> s -> (b, s)

你可能选择将其视为
f :: a -> ( s -> (b, s) )

在Haskell中,这与函数组合相同(因为函数组合是右结合的),它的含义是“f是一个接受类型为a的参数并返回具有状态的计算的函数”。这就是State单子的全部内容。

14

简短回答:

  1. State 旨在利用单子(monads)的特性,模拟带有本地变量的类似于命令式系统的状态。基本思想是在单子中隐藏每个步骤中获取当前状态并返回新状态以及中间结果的活动(这里我们有 s -> (a,s))。
  2. 不要将任意函数与包含在 State 中的函数混淆。前者可以具有您想要的任何类型(如果您想要在状态单子中使用它们,则必须最终生成一些 State a)。而后者保持类型为 s -> (a,s) 的函数:这是由单子管理的传递状态层。
  3. 正如我所说,包含在 State 中的函数实际上是通过为 Monad (State s) 实例定义的 (>>=)return 来生成的。它的作用是通过代码调用传递状态。

第3点也是状态参数从实际使用的函数中消失的原因。

长回答:

状态单子已经在不同的论文中进行了研究,并且也存在于 Haskell 框架中(我现在不记得好的参考资料了,我会尽快添加它们)。

它遵循的思想是:考虑一个类型 data MyState = ...,其值保存系统的当前状态。

如果您想通过一堆函数将其传递下去,您应该以这样的方式编写每个函数,即至少以当前状态作为参数之一,并返回带有其结果的一对(取决于状态和其他输入参数)和新的(可能修改过的)状态。好吧,这正是状态单子的类型告诉你的:s -> (a, s)。在我们的示例中,sMyState,旨在传递系统的状态。

封装在State中的函数除了当前状态外不需要参数,它会产生一个新状态和一个中间结果。您在示例中看到的具有更多参数的函数不是问题,因为当您在单子内使用它们的do符号时,您将把它们应用于所有"额外"所需的参数,这意味着每个参数都将产生一个部分应用的函数,其唯一剩余的参数是状态;State的单子实例将完成其余部分。
如果您查看可以在单子中使用的函数(实际上,在单子中它们通常被称为操作)的类型,您将看到它们的返回类型包含在单子中:这是告诉您的点,一旦您给予它们所有参数,它们实际上不会返回结果,而是(在这种情况下)返回一个函数s -> (a,s),该函数适合于单子的组合法则。
通过将系统的第一个/初始状态传递给整个块/组合来执行计算。
最后,不需要参数的函数将具有类似于State a的类型,其中a是它们的返回类型:如果您查看State的值构造函数,您将再次看到这实际上是一个函数s -> (a,s)

我刚刚看了你之前的回答,觉得它已经很完美了。但我错了,现在更好了 :) 恭喜。 - byrondrossos

6

我对Haskell完全是个新手,也无法很好地理解那本书中的State Monad代码。但是,让我在这里添加一下我的答案,以帮助未来的某些人。

答案:

  • State Monad的目标是什么?

    组合函数 来处理有状态的计算。
    例如:push 3 >>= \_ -> push 5 >>= \_ -> pop


  • 为什么pop不需要参数,而建议函数f需要一个参数?

    pop不需要参数,因为它被State包装了起来。
    类型为s -> (a, s)的解封装函数需要一个参数。同样适用于push
    您可以使用runState进行解封装。

    runState pop :: Stack -> (Int, Stack)
    runState (push 3) :: Stack -> ((), Stack)
    

    如果你指的是>>=右侧的“函数f”,那么f将会像\a -> pop\a -> push 3这样,而不仅仅是pop


详细解释:

这些是帮助我更好地理解State Monad和Stack示例的三个要点。

  • 考虑 bind 操作符(>>=)的参数类型

    Monad 类型类中 bind 操作符的定义如下:

    (>>=) :: (Monad m) => m a -> (a -> m b) -> m b

    在 Stack 的例子中,mState Stack。将 m 替换为 State Stack,则定义可以表示为:

    (>>=) :: State Stack a -> (a -> State Stack b) -> State Stack b 

    因此,bind 操作符左侧参数的类型将是 State Stack a,右侧参数的类型将是 a -> State Stack b

  • 将 do notation 翻译为 bind 操作符

    以下是使用 do notation 的示例代码:

    stackManip :: State Stack Int  
    stackManip = do  
         push 3  
         pop  
         pop  
    

    可以用 bind 操作符将其翻译成以下代码:

    stackManip :: State Stack Int  
    stackManip = push 3 >>= \_ -> pop >>= \_ -> pop
    

    现在我们可以看到 bind 操作符的右侧将是什么。
    它们的类型是 a -> State Stack b

    (\_ -> pop) :: a -> State Stack Int
    (\_ -> push 3) :: a -> State Stack ()
    


  • 认识在实例声明中 (State s)(State h) 的区别

    以下是书中 State 实例声明:

    instance Monad (State s) where  
        return x = State $ \s -> (x,s)  
        (State h) >>= f = State $ \s -> let (a, newState) = h s  
                                            (State g) = f a  
                                        in  g newState 
    

    考虑使用 Stack 时的类型,(State s) 的类型将是

    (State s) :: State Stack
    s :: Stack
    

    (State h) 的类型将是

    (State h) :: State Stack a
    h :: Stack -> (a, Stack)
    

    (State h) 是 bind 操作符的左侧参数,如上所述,它的类型是 State Stack a

    那么为什么 h 变成了 Stack -> (a, Stack)
    这是因为对新类型包装器中定义的 State 值构造函数进行模式匹配的结果,(State g) 也是如此。

    newtype State s a = State { runState :: s -> (a,s) }
    

    通常,h 的类型是表示具有状态的计算机的 s ->(a, s)。以下是 Stack 示例中可能的 h

    runState pop :: Stack -> (Int, Stack)
    runState (push 3) :: Stack -> ((), Stack)
    runState stackManip :: Stack -> (Int, Stack)
    

    就是这样。


5

State单子本质上是

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

从一个状态(s)到期望结果(a)和新状态的一对结果的函数。实现使状态的线程隐式,为您处理状态传递和更新,因此没有意外将错误的状态传递到下一个函数的风险。
因此,在State s单子中,一个接受k > 0个参数(其中一个是状态)并返回某些内容和新状态的一对内容的函数变成了一个接受k-1个参数并返回单子操作的函数(这基本上是一个函数,只需要一个参数——状态)。
在非状态设置中,pop接受一个参数,即堆栈,它是状态。因此,在单子设置中,pop变成一个不带显式参数的State Stack Int操作。
使用State单子而不是显式状态传递可以生成更清洁、出错机会更少的代码,这就是State单子实现的目的。所有这些都可以不用它完成,但这将更加繁琐且容易出错。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接