Haskell中未求值putStrLn是否会导致递归调用内存/空间泄漏
问题结论
每次调用func True时确实会产生空间泄漏,最终会耗尽全部可用内存。
原因分析
给出的示例代码使用Haskell的do记法编写,脱糖后的实际等价逻辑如下:
func bool = (if bool then func True else pure ()) >> putStrLn "hello"
空间泄漏的核心成因有两点:
- IO Monad的
(>>)组合子严格遵循「先完整执行左操作数,再执行右操作数」的顺序语义:当传入参数为True时,函数会进入then分支无限递归调用func True,永远不会走到else分支的pure (),因此所有挂载在递归调用右侧的putStrLn "hello"动作永远没有机会被执行。 - 该递归调用不属于尾调用:Haskell仅对位于do块最后位置的IO递归做尾调用优化。示例代码里每一层递归结束后都绑定了一个待执行的
putStrLn "hello"动作,运行时必须持续保存这些未执行动作的引用,GC无法回收这部分内存。随着递归层数无限增长,堆积的未求值thunk会持续占用内存,最终触发内存耗尽。
修复方式
要避免这类空间泄漏,只需要把递归调用放到do块的最后一个位置,让运行时可以触发尾递归优化即可。例如如果需要每次递归都输出hello,可以改写为:
func bool = do putStrLn "hello" if bool then func True else pure ()
这时候脱糖后的代码为putStrLn "hello" >> (if bool then func True else pure ()),递归调用位于尾位置,运行时不会堆积待执行动作,内存占用会保持恒定。
内容的提问来源于stack exchange,提问作者Anon
相关产品推荐
相关产品推荐

