Haskell中的评估和空间泄漏
我正在学习Haskell,目前正试图将我的头围绕monad。 在玩一些随机数字时,我又一次怠惰评估。 为了简化以下几点:
roll :: State StdGen Int
roll = do
gen <- get
let (n, newGen) = randomR (0,1) gen
put newGen
return n
main = do
gen <- getStdGen
let x = sum $ evalState (replicateM iterations roll) gen
print x
变成这样的东西:
roll' :: IO Int
roll' = getStdRandom $ randomR (0,1)
main = do
x' <- fmap sum $ replicateM iterations roll'
print x'
在大量iterations
,我们假设1000 * 1000 * 10
,第二个例子导致堆栈溢出。
为什么第一个版本快速运行在恒定的空间,第二个版本爆炸?
更广泛地说,你能推荐一些阅读材料来改进哈斯克尔懒惰评估的心理模型吗? (最好介绍中间级别。)因为当涉及到Haskell的评估时,我的直觉完全失败了。
这是因为Control.Monad.State
重新导出Control.Monad.State.Lazy
。 如果你导入了, Control.Monad.State.Strict
,都会以这种方式溢出。
严格的State
或IO
溢出的原因是, replicateM
需要递归运行iterations
次数,然后才能构建列表。 松散地说, replicateM
必须将它复制的所有动作的“效果”“组合”成一个巨大的“效果”。 术语“组合”和“效果”非常含糊,可以表示无数不同的事物,但它们是关于我们谈论这种抽象事物的最佳选择。 具有较大值的replicateM
最终会在几乎所有的monad选项中溢出堆栈。 这是事实,它不与懒惰State
,这是奇怪的。
要明白为什么它不会与懒惰State
一起溢出,您需要查看lazy State
和replicateM
的详细信息(>>=)
。 以下定义被大大简化,但它们反映了说明这是如何工作的必要细节。
newtype State s a = State { runState :: s -> (a, s) }
instance Monad (State s) where
return x = State $ s -> (x, s)
x >>= f = State $ s -> let (a, s') = runState x s in runState (f a) s'
replicateM :: Monad m => Int -> m a -> m [a]
replicateM 0 _ = return []
replicateM n mx | n < 0 = error "don't do this"
| otherwise =
mx >>= x -> replicateM (n - 1) mx >>= xs -> return (x:xs)
所以首先看看replicateM
。 请注意,当n
大于0时,它是(>>=)
的调用。 所以replicateM
的行为取决于什么(>>=)
。
当你看着(>>=)
你看它产生结合状态转换函数的结果的状态转换函数x
在let绑定,然后返回转换功能的结果的结果f
从应用于参数那种约束力。
好的,这句话很明显是泥巴,但它非常重要。 让我们来看看lambda的内幕。 查看函数(>>=)
创建,您可以看到let {something to do with x} in {something to do with f and the results of the let binding}
。 这对懒惰评估很重要。 这意味着它可能会忽略x
,或者可能是它的一部分,当它评估(>>=)
,如果特定函数f
允许。 在懒惰State
的情况下,这意味着它可能能够延迟计算未来的状态值,如果f
在查看状态之前可以产生构造函数。
事实证明,这是它的工作原理。 replicateM
组装调用(>>=)
的特殊方式,它会生成一个函数,在检查传递给它们的状态之前生成(:)
构造函数。 这允许增量处理列表,如果最终状态从不被检查。 如果你看到最终状态,那会破坏增量运行的能力,因为最终状态需要做所有的工作来计算它。 但是,使用evalState
导致最终状态被evalState
,因此评估可以自由进行。
罪魁祸首深藏在replicateM
里面。 让我们看看源代码。
replicateM :: (Monad m) => Int -> m a -> m [a]
replicateM n x = sequence (replicate n x)
sequence :: Monad m => [m a] -> m [a]
sequence ms = foldr k (return []) ms where
k m m' = do { x <- m; xs <- m'; return (x:xs) }
具体来说,请按sequence
单独展开foldr
foldr k (return []) (replicate n roll')
do x <- roll'
xs <- foldr k (return []) (replicate n roll')
return (x:xs)
换句话说,除非我们可以尽早地返回(x : ... thunk ... )
,否则我们将在返回第一个值之前展开整个复制。 关于我们是否可以返回该值的答案与我们的monad中的(>>=)
的定义有关。
roll' >>= x -> foldr k (return []) (replicate n roll') >>= xs -> return (x:xs)
可以这么说,因为IO
执行的是副作用,它会按顺序执行绑定 - 我们一定会展开整个事情。 State
有两种形式, Control.Monad.Trans.State.Lazy
版本和Control.Monad.Trans.State.Strict
版本,其中Control.Monad.Trans.State
默认为Lazy
版本。 在那里, (>>=)
被定义为
m >>= k = StateT $ s -> do
~(a, s') <- runStateT m s
runStateT (k a) s'
所以我们可以看到显式的无可辩驳的绑定发生,这让我们继续懒惰地返回结果。
值得关注一下Joachim Breitner最近对这个问题的回顾。 pipes
和conduit
生态系统中还有许多工作值得研究。
一般来说,值得怀疑replicateM
是由于我们在上面看到的排序概念:“构建头部然后构建尾部然后返回缺点”。
上一篇: Evaluation and space leaks in Haskell
下一篇: Why is seq bad?