没有什么会迫使中间计算通过列表的中间。如果将last 替换为sum,它将停止泄漏内存。
要了解发生了什么,让我们考虑一个稍微简单的同一件事的陈述:
main = mapM (print . last) $ scanl incState (replicate 5 0) (repeat 1)
现在,让我们开始评估它。首先,我们状态的内存看起来像
scanl incState (map (+1) (replicate 5 0)) (repeat 1)
我们将跳过一些细节,但我们的第一个要求是mapM 模式与状态列表的开头匹配。
(:) -> replicate 5 0
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将last 项目变为print,我们强制列表的脊椎和最后一个项目,而不是单个项目。我们将在下一步中更好地看到这一点。
0 : 0 : 0 : 0 : 0 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
对于mapM 的下一步,我们需要状态列表中的下一项。内存现在看起来像
0 : 0 : 0 : 0 : 0 : []
^
|________
|
(:) -> map (+1) |
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将 last 项目变为 print,我们强制列表的脊椎和最后一个项目,而不是其他单个项目。
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
对于mapM 的下一步,我们需要状态列表中的下一项。内存现在看起来像
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^
|________
|
(:) -> map (+1) |
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将last 项目变为print,我们强制列表的脊椎和最后一个项目,而不是其他单个项目。
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 2 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
每次重复此操作时,我们都会留下另一个 thunk 列表,参考之前的 thunk 列表等。这就是空间泄漏的来源。如果您在每一步之后强制所有结果,就像print . sum 所做的那样,就不会有空间泄漏。