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

2023-12-19

我正在研究 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

Note我只想知道这段代码中出现问题的原因是什么,任务本身并不重要。


问题是 Control.Monad.State.Lazy 的 (>>=) 太懒了,甚至 ($!) 也帮不了你。
尝试 Control.Monad.State.Strict,它应该达到 ($!)。

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

好吧,我已经吃过了,现在我可以详细说明了。让我使用惰性的旧(mtl-1.x)定义State smonad,在没有内部 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'

Now, 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.

In replicateM r tick,计算与 (>>) 链接在一起,因此k(>>=) 的定义是const tick。作为一个常量函数,它绝对不需要检查它的参数。所以tick >> tick becomes

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

the seq直到blobN必须进行评估。但需要将其评估为最外层的构造函数 - 对构造函数(,)- 足以触发 seq,进而导致此处完成评估。现在,在million,在最终之前不需要任何评估snd之后runState到达了。到那时,一个百万层的thunk已经建成了。评估那个重击需要推动很多let m' = m+1 in seq m' ((),m')在堆栈上直到达到初始状态,如果堆栈足够大可以容纳它们,那么它们就会被弹出并应用。所以这将是三个遍历,1.构建thunk,2.从thunk中剥离层并将它们推入堆栈,3.消耗堆栈。

Control.Monad.State.Strict 的 (>>=) 严格到足以强制seq每个绑定上都有 s,因此只有一次遍历,不会构建(非平凡的)thunk,并且计算在恒定空间中运行。 定义是

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

重要的区别是模式匹配case表达式是严格的,这里blob必须评估最外面的构造函数以将其与中的模式相匹配case.
With 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,每次绑定时都会对所有内容进行全面评估,没有重击,没有堆栈。

但是,您仍然需要小心。在这种情况下,由于seq (resp. ($!))以及所涉及类型的浅层结构,评估跟上应用(>>)。一般来说,具有更深层次的结构化类型和/或不具有seqs、C.M.S.Strict 还会构建大型 thunk,这可能导致堆栈溢出。在这些情况下,与 C.M.S.Lazy 生成的 thunk 相比,thunk 更简单,也更少纠缠。

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

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

终止。 [但请注意,此时状态将无法使用;在使用它之前,它必须遍历整个无限列表。因此,如果您执行类似操作,则在恢复状态相关计算之前,您必须put新鲜的状态。]

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么简单地使用 State monad 会导致堆栈溢出? 的相关文章

  • Haskell 入门

    这个问题的答案是社区努力 help privileges edit community wiki 编辑现有答案以改进这篇文章 目前不接受新的答案或互动 几天来 我一直试图理解 Haskell 中的函数式编程范例 我通过阅读教程和观看截屏视频
  • Haskell 中的尾递归字符串分割

    我正在考虑分割字符串的问题s在一个字符处c 这表示为 break c s 其中 Haskell 库定义break c 足够接近 br br s h t if c h then s else let h t br t in h h t 假设我
  • Traversable 类型类的用途

    有人可以向我解释一下类型类的目的是什么吗Traversable 类型类定义是 class Functor t Foldable t gt Traversable t gt where So Traversable is a Functor
  • 有没有更好的方法将 UTC 时间转换为大纪元时间?

    我想将文件的修改时间设置为从 exif 数据获取的时间 为了从 exif 获取时间 我发现 Graphics Exif getTag Exif gt String gt IO Maybe String 要设置文件修改时间 我发现 Syste
  • 在VS2015中构建项目:“csc.exe”退出,代码为-1073741571

    我使用的是Visual studio 2013 昨天 我安装了VS2015 企业更新3 我的解决方案的构建过程在 VS2015 中的一个项目中崩溃了 VS2017 RC 也会出现同样的异常 该解决方案在 VS2013 中成功构建 该解决方案
  • 你能识别 Haskell 程序中的无限列表吗? [复制]

    这个问题在这里已经有答案了 可能的重复 如何判断列表是否是无限的 https stackoverflow com questions 7371730 how to tell if a list is infinite 在Haskell中 你
  • Haskell:IORef 的性能

    我一直在尝试在 Haskell 中编码一个需要使用大量可变引用的算法 但与纯粹的惰性代码相比 它 也许并不奇怪 非常慢 考虑一个非常简单的例子 module Main where import Data IORef import Contr
  • 这个对自身单位的列表理解是如何工作的?

    在 haskell IRC 频道中有人问 是否有一种简洁的方法来定义一个列表 其中第 n 个条目是之前所有条目的平方和 我认为这听起来像一个有趣的谜题 递归定义无限列表是我真正需要练习的事情之一 所以我启动了 GHCi 并开始尝试递归定义
  • 为什么 ZipList 不是 List 的默认应用实例

    我目前正在学习 Haskell 中的应用程序 如果我没记错的话 列表有两个不同的应用实例 List and ZipList 第二个被定义为包装列表值的新类型 这ZipList应用实例对我来说似乎更直观 这可能是一个愚蠢的问题 但有具体原因吗
  • 我该如何实现这个折叠功能呢?

    给出了两种数据类型 颜色 和 植物 data Color Red Pink White Blue Purple Green Yellow deriving Show Eq data Plant Leaf Blossom Color Stal
  • 在 monad 转换器类型类中使用列表 monad?

    我的目标是创建一个在 ReaderT WriterT 堆栈或 RWS 堆栈中使用列表 monad 的函数 更一般地说 如何在 mtl 类型类 例如 MonadReader MonadWriter 中使用列表 monad 我为什么要尝试这样做
  • Data.Sequence 中的 inits 和 tails 如何工作?

    Louis Wasserman 编写了当前的实现inits and tails in Data Sequence 他表示它们非常高效 事实上 只要查看代码 我就可以看到 无论它们在做什么 它们都是以干净 自上而下的方式进行的 这往往会给惰性
  • 迭代打印列表中的每个整数

    假设我有一个整数列表l 1 2 我想打印到stdout Doing print l产生 1 2 假设我想打印不带大括号的列表 map print l产生 No instance for Show IO arising from a use
  • 纯 Haskell 代码需要线程池吗?

    In 现实世界 Haskell 第 28 章 软件事务内存 http book realworldhaskell org read software transactional memory html 开发了一个并发网络链接检查器 它获取网
  • 为什么在 where 子句中使用类型签名如此罕见?

    它是否有助于编译器优化 或者只是添加额外类型签名的多余工作 例如 人们经常看到 foo a gt b foo x bar x where bar x undefined 而不是 foo a gt b foo x bar x where ba
  • RankN多态性和令人发指的克莱斯利之箭

    我不明白为什么 demobind1 的定义会产生一些编译器错误 它看起来像一个愚蠢的翻转 但不知何故 LANGUAGE GADTs LANGUAGE RankNTypes ScopedTypeVariables TypeOperators
  • 使用带有两个列表而不是一个列表的地图。可以筑巢吗?

    我需要多次运行一个带有两个参数的函数 我有两个包含这些参数的列表 我希望能够使用map或类似的东西用相应的参数调用函数 我要调用的函数具有以下类型 runParseTest String gt String gt IO 列表的创建方式如下
  • Haskell 处理负参数

    尝试对两个值求和 其中只有一个为负值 例如 1 and 2 soma Float gt Float gt Float soma x1 x2 x1 x2 结果出现错误 为什么
  • 诊断 RangeError:React KeyEscapeUtils 中超出了最大调用堆栈大小

    背景 我们的 Web 应用程序是使用官方的 React Redux 绑定用 React 和 Redux 编写的 此网络应用程序中使用的另一个主要库是PaperJS http paperjs org 我们最近将其转变为 Redux 应用程序
  • Haskell 中多核编程的现状如何?

    Haskell 中多核编程的现状如何 现在有哪些项目 工具和库可用 有哪些经验报道 2009年至2012年期间 发生了以下事件 2012 从 2012 年开始 并行 Haskell 状态更新开始出现在并行 Haskell 摘要 http w

随机推荐