【问题标题】:Why does this simple use of the State monad cause a stack overflow?为什么简单地使用 State monad 会导致堆栈溢出?
【发布时间】:2011-12-21 08:35:44
【问题描述】:

我在玩 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

注意 我只是想知道是什么导致了这段代码的问题,任务本身并不重要。

【问题讨论】:

  • 与您的实际问题无关:您可能喜欢replicateM
  • 我看到了import Control.Monad.State.Lazyput $! (n+1),我立即感到怀疑......
  • @DanBurton 一开始其实是Control.Monad.State,后来发现和C.M.S.Lazy一样所以改了。我忘记了所有关于C.M.S.Strict 的事情:)

标签: haskell stack-overflow monads state-monad


【解决方案1】:

问题是 Control.Monad.State.Lazy 的 (>>=) 太懒了,甚至 ($!) 也无济于事。
试试 Control.Monad.State.Strict,应该会达到 ($!)。

惰性状态 monad 的 (>>=) 根本不查看 (value,state) 对,因此在到达结束之前完成一些评估的唯一方法是在 fm &gt;&gt;= f 解构这对。这不会在这里发生,所以你会得到一个巨大的 thunk,当 runState 最终想要一个结果时,这对于堆栈来说太大了。

好的,我已经吃过了,现在我可以详细说明了。让我使用惰性 State s monad 的旧 (mtl-1.x) 定义,没有内部 monad 会更容易看到。新的 (mtl-2.x) 定义 type State s = StateT s Identity 行为相同,只是更多的写作和阅读。 (>>=) 的定义是

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

现在,let 绑定是惰性的,因此这是

m >>= k = State $ \s -> let
    blob = runState m s
    in runState (k $ fst blob) (snd blob)

只是更具可读性。所以 (>>=) 让 blob 完全不被评估。仅当k 需要检查fst blob 以确定如何继续,或者k a 需要检查snd blob 时才需要评估。

replicateM r tick 中,计算与(>>) 链接在一起,因此(>>=) 中的k 定义为const tick。作为一个常量函数,它绝对不需要检查它的参数。所以tick &gt;&gt; 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 中,在达到runState 之后的最终snd 之前,不需要任何评估。到那时,已经构建了具有一百万层的thunk。评估该 thunk 需要在堆栈上推送许多 let m' = m+1 in seq m' ((),m') 直到达到初始状态,如果堆栈大到足以容纳它们,它们就会被弹出并应用。所以这将是三个遍历,1. 构建 thunk,2. 从 thunk 中剥离层并将它们推送到堆栈上,3. 消耗堆栈。

Control.Monad.State.Strict 的 (>>=) 足够严格,可以在每次绑定时强制使用 seq ,因此只有一次遍历,没有构建(非平凡的)thunk 并且计算运行在恒定的空间。 定义是

m >>= k = State $ \s ->
    case runState m s of
      (a, s') -> runState (k a) s'

重要的区别是case 表达式中的模式匹配是严格的,这里blob 必须被评估为最外层的构造函数以将其与case 中的模式匹配。
有了m = tick = State (\m -&gt; 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') [对 (,) 构造函数] 的评估,由与s' = s+1 评估相关联的seq,在每次绑定时都完全评估,没有重击,没有堆栈。

但是,您仍然需要小心。在这种情况下,由于seq(分别为($!))和所涉及类型的浅层结构,评估跟上了(&gt;&gt;)的应用。通常,对于更深层次的结构化类型和/或没有seqs,C.M.S.Strict 还会构建可能导致堆栈溢出的大型 thunk。在这种情况下,与 C.M.S.Lazy 生成的 thunk 相比,thunk 更简单且更少纠缠。

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

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

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

【讨论】:

  • 非常感谢您的详细解释。我还在源代码中注意到C.M.S.Lazy 使用惰性模式,而C.M.S.Strict 没有,这就是导致当前版本差异的原因。不过,您对旧版本的解释更清楚,再次感谢。
  • 在您的回答 here 中,您必须明确使用惰性模式匹配,但在您上面的解释中,您提到 let 绑定是惰性的。能否请您详细说明这两种情况的区别?
  • 在那个答案中,惰性模式是一个函数参数。函数定义中的函数参数 - 无论函数是否绑定在 let 中 - 在调用函数时都会导致模式匹配。模式匹配是严格的,除非模式是无可辩驳的(变量、通配符或惰性模式 ~pattern)。由于那里的函数然后成为fix 的参数,它不能是严格的,所以它的参数必须是一个无可辩驳的模式。除了~(st:sts),可以使用变量并用headtail 解构它,但~(st:sts) 更好。
猜你喜欢
  • 1970-01-01
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-12-30
  • 2018-07-01
  • 2010-09-11
  • 1970-01-01
  • 2011-01-13
相关资源
最近更新 更多