作为一种纯函数式编程语言,Haskell密集使用递归。 Haskell中是否出现堆栈溢出错误,就像在Java中一样?为什么,或为什么不呢?
答案 0 :(得分:12)
由于懒惰,Haskell使用不同于Java的堆栈。
在Java中,在调用方法时会创建堆栈帧,并在方法返回时释放。因此,如果f()
是一个递归方法,则每次递归调用f()
都会生成一个堆栈帧,并且这些帧是严格嵌套的。当你有一连串递归调用时,你可以获得堆栈溢出,例如f() -> f() -> f() -> …
。
而在Haskell中,在调用函数时会创建 thunk 。当使用模式匹配(例如case
)强制thunk时创建堆栈帧,并且当thunk的评估完全足以返回值(可能包含更多未评估的thunk)时释放堆栈帧。
因此,如果f
是递归函数,则每次调用f
都会生成一个thunk,并且case
会在结果上生成一个堆栈帧,但这些帧是当thunk之间存在依赖关系时,只嵌套。事实上,这就是seq
原语的作用:a `seq` b
表示“在a
之前评估b
,返回b
”,但您也可以想到它在b
上添加a
的依赖关系,因此在评估b
时,a
也会被强制使用。
当您有一个 thunks 的深链来评估时,您可以获得堆栈溢出,例如在过于懒惰的foldl
函数中:
foldl (+) 0 [1..5]
==
foldl (+) 0 (1 : 2 : 3 : 4 : 5 : [])
==
((((0 + 1) + 2) + 3) + 4) + 5
这会产生一系列像这样的thunk:
((+)
((+)
((+)
((+)
((+)
0
1)
2)
3)
4)
5)
当我们强制执行结果时(例如,通过打印它),我们需要一直向下移动此链,以便能够<{>>开始评估它,(+) 0 1
咚。
因此foldl
经常会为大输入产生堆栈溢出,这就是为什么在需要左关联折叠时大多数时候应该使用foldl'
(严格)。 foldl'
不是构建嵌套thunk的链,而是立即评估中间结果(0+1 = 1
,1+2 = 3
,3+3 = 6
,...)。
答案 1 :(得分:-1)
在Haskell中不会发生堆栈溢出,就像在Java等中一样,因为评估和函数调用的发生方式不同。
在Java和C以及其他类似语言中,函数调用是使用调用堆栈实现的。当你调用一个函数时,所有函数的局部变量都被分配到一个调用堆栈中,当函数完成时,该函数会从堆栈中弹出。嵌套调用太多,调用堆栈将溢出。
在Haskell中,函数调用的工作原理并不一定。在大多数编译器中,如GHC,Haskell函数调用不使用调用堆栈实现。它们是使用完全不同的进程实现的,使用堆上的thunk分配。
因此,大多数haskell实现首先没有为函数调用实现调用堆栈,因此堆栈溢出的想法是非敏感的。这就像谈论在更衣室里只有淋浴的浴缸溢出来。
(GHC确实使用调用堆栈进行thunk评估,但不使用函数调用。因此,堆栈溢出可能与Java,C等堆栈溢出完全不同且无关。)