Haskell中是否发生堆栈溢出错误?

时间:2017-04-06 20:29:33

标签: haskell recursion stack-overflow

作为一种纯函数式编程语言,Haskell密集使用递归。 Haskell中是否出现堆栈溢出错误,就像在Java中一样?为什么,或为什么不呢?

2 个答案:

答案 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 = 11+2 = 33+3 = 6,...)。

答案 1 :(得分:-1)

在Haskell中不会发生堆栈溢出,就像在Java等中一样,因为评估和函数调用的发生方式不同。

在Java和C以及其他类似语言中,函数调用是使用调用堆栈实现的。当你调用一个函数时,所有函数的局部变量都被分配到一个调用堆栈中,当函数完成时,该函数会从堆栈中弹出。嵌套调用太多,调用堆栈将溢出。

在Haskell中,函数调用的工作原理并不一定。在大多数编译器中,如GHC,Haskell函数调用使用调用堆栈实现。它们是使用完全不同的进程实现的,使用堆上的thunk分配。

因此,大多数haskell实现首先没有为函数调用实现调用堆栈,因此堆栈溢出的想法是非敏感的。这就像谈论在更衣室里只有淋浴的浴缸溢出来。

(GHC确实使用调用堆栈进行thunk评估,但不使用函数调用。因此,堆栈溢出可能与Java,C等堆栈溢出完全不同且无关。)