我正在玩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
注意 我只想知道这段代码中导致问题的原因,任务本身并不重要。
答案 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 tick
为k
。作为一个常数函数,它绝对不需要检查它的论点。所以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
一个新的状态。]