为什么简单地使用State monad会导致堆栈溢出?

时间:2011-11-03 16:30:12

标签: haskell stack-overflow monads state-monad

我正在玩State monad,我不知道在这段简单的代码中是什么导致了堆栈溢出。

import Control.Monad.State.Lazy

tick :: State Int Int
tick = do n <- get
         put $! (n+1)
         return n

million :: Int
million = snd $ runState (mapM_ (const tick) [1..1000000]) 0

main = print million

注意 我只想知道这段代码中导致问题的原因,任务本身并不重要。

1 个答案:

答案 0 :(得分:41)

问题是Control.Monad.State.Lazy(&gt;&gt; =)是如此懒惰甚至($!)对你没有帮助。
尝试Control.Monad.State.Strict,它应该到达($!)。

懒惰状态monad的(&gt;&gt; =)并未查看(value,state)对,因此在达到结束之前完成某些评估的唯一方法是使用{{ 1 {}在f中解构了这对。这种情况在这里不会发生,因此当runState最终想要一个结果时,你会得到一个巨大的thunk,这对于堆栈来说太大了。

好的,我已经吃过了,现在我可以详细说明了。让我使用懒惰m >>= f monad的旧(mtl-1.x)定义,在没有内部monad的情况下更容易看到它。新的(mtl-2.x)定义State s表现相同,只是更多的写作和阅读。 (&gt;&gt; =)的定义是

type State s = StateT s Identity

现在,m >>= k = State $ \s -> let (a, s') = runState m s in runState (k a) s' 绑定是懒惰的,因此这是

let

只是更具可读性。所以(&gt;&gt; =)让blob完全没有评估。仅当m >>= k = State $ \s -> let blob = runState m s in runState (k $ fst blob) (snd blob) 需要检查k以确定如何继续,或fst blob需要检查k a时,才需要进行评估。

snd blob中,计算与(&gt;&gt;)链接,因此(&gt;&gt; =)定义中的replicateM r tickk。作为一个常数函数,它绝对不需要检查它的论点。所以const tick变成了

tick >> tick

State $ \s -> let blob1 = (\n -> let n' = n+1 in seq n' ((),n')) s blob2 = (\m -> let m' = m+1 in seq m' ((),m')) (snd blob1) in blob2 必须进行评估之前,不会触及seq。但是需要将它评估到最外层的构造函数 - 对构造函数blobN - 足以触发seq,这将导致在此完成评估。现在,在(,)中,在达到million之后的最终snd之前,不需要任何评估。到那时,已经建造了一个拥有一百万层的thunk。评估thunk需要在堆栈上推送许多runState直到达到初始状态,并且如果堆栈足够大以容纳它们,则它们将被弹出并应用。所以它是三次遍历,1。构建thunk,2。从thunk剥离层并将它们推到堆栈上,3。消耗堆栈。

Control.Monad.State.Strict的(&gt;&gt; =)严格到足以在每个绑定上强制let m' = m+1 in seq m' ((),m'),因此只有一个遍历,没有(非平凡)thunk被构建并且计算在恒定的空间内运行。 定义是

seq

重要的区别在于m >>= k = State $ \s -> case runState m s of (a, s') -> runState (k a) s' 表达式中的模式匹配是严格的,这里必须将case计算到最外层的构造函数,以使其与blob中的模式匹配。登记/> case m = tick = State (\m -> let m' = m+1 in seq m' ((),m'))必不可少的部分

case let s' = s+1 in seq s' ((),s') of
    (a, s'') -> runState (k a) s''

模式匹配要求((), s') [对(,)构造函数]的评估,与seq的评估相关联的s' = s+1,所有内容都在每个绑定,没有thunk,没有堆栈。

但是,你还需要小心。在这种情况下,由于seq(resp。($!))和所涉及类型的浅层结构,评估与(>>)的应用保持一致。通常,对于更深层次的结构化类型和/或没有seq s,C.M.S.Strict也会构建大的thunk,这可能导致堆栈溢出。在那种情况下,这些thunk比C.M.S.Lazy生成的那些更简单,更少纠缠。

另一方面,C.M.S.Lazy的懒惰允许C.M.S.Strict无法进行的其他计算。例如,C.M.S.Lazy提供了极少数monad之一

take 100 <$> mapM_ something [1 .. ]

终止。 [但请注意,该州无法使用;在它可以使用之前,它必须遍历整个无限列表。所以,如果你做了类似的事情,在恢复依赖状态的计算之前,你必须put一个新的状态。]