【问题标题】:How does the do notation in Monad mean in HaskellMonad中的do符号在Haskell中是什么意思
【发布时间】:2020-06-14 11:10:32
【问题描述】:

考虑以下代码段

import Control.Monad.State

type Stack = [Int]

pop :: State Stack Int 
pop = state $ \(x:xs) -> (x, xs)

push :: Int -> State Stack () 
push a = state $ \xs -> ((), a:xs)

stackManip :: State Stack Int
stackManip = do 
    push 3 
    a <- pop 
    pop

众所周知,Monad 中的 do notation&gt;&gt;= 运算符相同。我们可以将此段改写为:

push 3 >>= (\_ ->
pop >>= (\a ->
pop))

这个表达式的最终值与push 3和第一个pop无关,无论输入什么,它都只是返回pop,所以不会先将值3入栈并弹出它,为什么会发生这种情况?


感谢您的回复。我在上面添加了缺少的代码(Stackpushpop 的实现),我想我已经弄清楚它是如何工作的。理解这段代码的关键是理解State smonad的实现:

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

&gt;&gt;=do 的整个实现是将状态传递给函数h,计算其新状态并将新状态传递给函数g 隐含在 f 中,获得更新的状态。 所以实际上状态已经隐式改变了。

【问题讨论】:

  • 没有时间详细回答,但这主要是因为 State monad 的 &gt;&gt;= 是如何定义的。它确保当状态被一个表达式改变时,更新的值被传递给下一个。
  • Stackpushpop 是如何定义的?
  • “不会”是什么意思?当然会的,你在代码中写了很多。您首先推送 3,然后将其弹出(否定推送,是的,但编译器为什么要关心?在任何情况下它都是不可观察的(除非通过检查编译器生成的代码)),然后再执行一次弹出,返回其值(大概这就是pop 的定义方式)。所以等效的 sn-p 是push 3 &gt;&gt;= (\() -&gt; pop &gt;&gt;= (\3 -&gt; pop))。 (可能,如果这就是 push 的定义方式)。你的问题到底是什么? Haskell 是 YAPL,程序员负责。
  • State monad 的全部意义在于隐藏状态的显式传递(在这种情况下,是被操作的堆栈)。
  • @chepner 我认为你搞错了。它确实从 second pop AFAICS 返回值。它忽略了第一次弹出的值(这与它在第一次弹出之前刚刚推送的 3 相同)。

标签: haskell functional-programming monads state-monad do-notation


【解决方案1】:

Haskell 中的 Monad 有时被称为“可编程分号”。总的来说,这不是我觉得特别有用的短语,但它确实捕捉到了用 Haskell 的 do 表示法编写的表达式具有某种命令式程序风味的方式。特别是 do 块中的“语句”组合的方式取决于正在使用的特定 monad。因此,“可编程分号”——连续的“语句”(在许多命令式语言中由分号分隔)组合在一起的方式可以通过使用不同的 monad 来更改(“编程”)。

由于do 表示法实际上只是使用&gt;&gt;= 运算符从其他人构建表达式的语法糖,因此每个monad 的&gt;&gt;= 实现决定了它的“特殊行为”是什么。

例如,MaybeMonad 实例作为粗略的描述允许使用 Maybe 值,就好像它们实际上是基础类型的值一样,同时确保如果非值 (也就是说,Nothing) 发生在任何一点,计算短路和Nothing 将是整体结果。

对于列表单子,每一行实际上都被“执行”了多次(或没有)——列表中的每个元素都执行一次。

对于State s monad 的值,这些本质上是s -&gt; (a, s) 类型的“状态操作函数”——它们采用初始状态,并从中计算新状态以及某种类型的输出值@ 987654333@。 &gt;&gt;= 实现 - “分号” - 在这里* 只是确保,当一个函数 f :: s -&gt; (a, s) 后跟另一个 g :: s -&gt; (b, s) 时,生成的函数将 f 应用于初始状态,然后应用 @ 987654338@ 到从f 计算的状态。它基本上只是函数组合,稍作修改,以便我们也可以访问类型不一定与状态相关的“输出值”。这允许人们在do 块中一个接一个地列出各种状态操作函数,并且知道每个阶段的状态正是由前几行组合计算得出的状态。这反过来又允许一种非常自然的编程风格,在这种风格中,您可以连续发出“命令”来操纵状态,而无需实际进行破坏性更新,或者以其他方式脱离纯函数和不可变数据的世界。

*严格来说,这不是&gt;&gt;=,而是&gt;&gt;,这是一个派生自&gt;&gt;=但忽略输出值的操作。您可能已经注意到,在我给出的示例中,f 输出的 a 值被完全忽略 - 但&gt;&gt;= 允许检查该值并确定接下来要执行的计算。在do 表示法中,这意味着写入a &lt;- f,然后再使用a。这实际上是将 Monad 与其功能较弱但仍然很重要的表亲(尤其是 Applicative 函子)区分开来的关键。

【讨论】:

    【解决方案2】:

    由于 Monad Laws,您的代码相当于

    stackManip :: State Stack Int
    stackManip = do 
        push 3 
        a <- pop    -- a == 3
        r <- pop
        return r
    

    所以你按下 3,弹出它,忽略弹出的 3,弹出另一个值并返回它。

    Haskell 只是另一种编程语言。你程序员在控制。编译器是否跳过无关紧要的指令取决于它,并且无论如何都是不可观察的(除非通过检查编译器生成的代码,或者在执行代码时测量 CPU 的热量,但在服务器中可能有点难以做到极圈以外的农场)。

    【讨论】:

      猜你喜欢
      • 2012-02-26
      • 1970-01-01
      • 1970-01-01
      • 2022-01-02
      • 1970-01-01
      • 2010-09-29
      • 2011-04-18
      • 2020-08-31
      相关资源
      最近更新 更多