【问题标题】:Haskell — Monad binding evaluation orderHaskell — Monad 绑定求值顺序
【发布时间】:2013-04-10 12:52:15
【问题描述】:

我在弄清楚 /how/ 绑定运算符实际上将以下 State monad 绑定在一起时遇到了一些麻烦:

pop :: State [Int] Int
pop = do
        (x:xs) <- get
        put xs
        return x

push :: Int -> State [Int] ()
push x = do 
            xs <- get
            put (x:xs)

doStuff :: State [Int] ()
doStuff = do
            pop
            x <- pop
            push 5
            push x

doStuff,可脱糖为:

pop >>= (\_ -> pop >>= (\x -> push 5 >>= (\_ -> push x)))

当评估这一行时,绑定实际发生的顺序是什么?因为,要实际绑定,Haskell 需要从 &gt;&gt;= 运算符右侧的函数中获取一个 State monad(即,需要首先对函数右操作数进行完全评估),所以我认为会发生以下情况:

  1. s1 = push 5 &gt;&gt;= (\_ -&gt; push x)
  2. s2 = pop &gt;&gt;= (\x -&gt; s1)
  3. s3 = pop &gt;&gt;= (\_ -&gt; s2)

这是正确的思考方式吗?我觉得我很了解 monad,但我最大的问题是实际上可视化“幕后”发生的事情以及数据如何流动,可以这么说。 do 表示法给人一种我正在处理一堆顺序操作的错觉,而事实上,有一大堆嵌套和闭包。

我觉得我在这里想太多了,结果让自己更加困惑。

【问题讨论】:

  • 您应该从 state monad 扩展 &gt;&gt;= 的定义,以了解发生了什么。没有什么特别的,只是普通的评估。
  • 评估策略在这里并不重要,因此没有正确的方式来考虑它。通过尝试不同的评估方式,您可以很容易地看到这一点。结果总是一样的。

标签: haskell monads


【解决方案1】:

pop >>= (\_ -> pop >>= (\x -> push 5 >>= (\_ -> push x)))

可以内联一些函数(以更好地显示正在发生的事情)。我将从(&gt;&gt;=) 开始,假装State 没有定义为transformer 或newtype,以保持简单。

type State s a = s -> (a, s)
m >>= k = \ s -> let (a, s') = m s in k a s'

\ s -> let (a, s') = pop s in
(\ _ -> pop >>= (\ x -> push 5 >>= (\ _ -> push x))) a s'

\ s -> let (_, s') = pop s in
(pop >>= (\ x -> push 5 >>= (\ _ -> push x))) s'

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
(\ x -> push 5 >>= (\ _ -> push x)) a s''

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
(push 5 >>= (\ _ -> push a)) s''

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
let (b, s''') = push 5 s'' in
(\ _ -> push a)) b s'''


\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
let (_, s''') = push 5 s'' in
push a s'''

【讨论】:

    【解决方案2】:

    这是正确的思考方式吗?

    没有。

    首先:虽然在IO monad 中考虑“首先发生这种情况,然后我们评估这个键盘输入......”显然是正确的,但并非所有 monad 都是如此。例如,在 list monad 中,这实际上没有任何意义。一般来说,根本不可能为 Haskell 中的计算分配特定的顺序,这不是定义的行为。

    然而,在 monad 中考虑计算顺序总是可能,而且通常很有帮助,而这个顺序实际上是 do 符号所建议的顺序。因此,在大多数情况下,考虑脱糖表达实际上并没有洞察力。但如果你想迈出这一步,我会这样做:

    1. pop &gt;&gt;= \_ -&gt; THUNK1

    2. THUNK1 ≡> pop &gt;&gt;= \x -&gt; THUNK2

    3. {Closure{x}} THUNK2 ≡> push 5 &gt;&gt;= \_ -&gt; THUNK3

    4. {Closure{x}} THUNK3 ≡> push x

    这当然更丑陋,但说起来几乎和加糖的do 表达式一样。

    【讨论】:

      【解决方案3】:

      当评估这一行时,绑定实际发生的顺序是什么?

      这里的“绑定”没有什么特别之处。脱糖表达式的求值方式与任何其他表达式完全相同(惰性),具体取决于您正在使用的特定 monad 的 (&gt;&gt;=) 的实现。

      如果我们谈论使用runState 之类的东西,给定foo &gt;&gt;= (\x -&gt; bar) 之类的表达式,最外面的表达式是(&gt;&gt;=) 的应用,但我们试图解开newtype,然后应用函数在里面,所以 (&gt;&gt;=) 和函数一样被强制使用。

      如果我们考虑使用 list monad,(&gt;&gt;=)concatMap。给定像 [foo1, foo2] &gt;&gt;= (\x -&gt; [bar1, bar2, x] &gt;&gt;= (\y -&gt; [baz, y])) 这样的表达式,在结果上使用 take 5 显然不会完全计算所有绑定。


      也就是说,这里有一个重要规则:无论在何种程度上评估 x &gt;&gt;= f 都会强制评估 x,在像脱糖 do 块这样的大表达式中,强制将以明显的“顺序”顺​​序发生,出于同样的原因,“顺序错觉”是可能的。

      【讨论】:

        猜你喜欢
        • 2011-12-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多