函数式语言中的程序是否更有可能出现堆栈溢出?

时间:2009-08-18 01:52:16

标签: functional-programming stack-overflow tail-call-optimization imperative-programming

我开始学习ocaml,我非常欣赏语言中递归的力量。但是,我担心的一件事是堆栈溢出。

如果ocaml使用堆栈进行函数调用,它最终是否会溢出堆栈?例如,如果我有以下功能:

let rec sum x =
  if x > 1 then f(x - 1) + x
  else x;;

它最终必然会导致堆栈溢出。如果我要在c ++中使用相同的东西(使用递归),我知道它会溢出。

所以我的问题是,是否有内置的安全措施来阻止函数式语言溢出堆栈?如果不是这样,它们是不是没那么有用,因为上面的求和算法,用带有for循环的过程样式编写,可以处理任何数字(不相关的整数溢出)?

5 个答案:

答案 0 :(得分:10)

所有(体面实现的;-)函数式语言优化尾递归,但这不是你在这里做的,因为递归调用不是LAST操作(需要后跟添加)。

所以,很快就会学会使用一个IS尾递归的辅助函数(并将当前的总数作为一个参数累积),这样优化器就可以完成它的工作,即可能的O'Caml语法。生锈:

let sum x =
  aux(x)(0);;

let rec aux x accum =
  if x > 1 then aux(x - 1)(accum + x)
  else (accum + x);;

这里,总和作为递归调用的ARGUMENT发生,即在递归本身之前,因此尾部优化可以启动(因为递归是最后需要发生的事情!)。

答案 1 :(得分:6)

功能语言通常具有更大的堆栈。例如,我已经编写了一个专门用于测试OCaml中的堆栈限制的函数,并且在它被禁止之前它已经超过10,000个调用。但是,您的观点是有效的。堆栈溢出仍然是功能语言中需要注意的事项。

函数式语言用于减轻其对递归的依赖的策略之一是使用tail-call optimization。如果对当前函数的下一个递归的调用是函数中的最后一个语句,则可以从堆栈中丢弃当前调用,并在其位置实例化新调用。生成的汇编指令与命令式样式的while循环基本相同。

您的函数不是尾部调用可优化的,因为递归不是最后一步。它需要先返回,然后才能将x添加到结果中。通常这很容易解决,您只需创建一个辅助函数,将累加器与其他参数一起传递

let rec sum x =
  let sum_aux accum x =
    if x > 1 then sum_aux (accum + x) (x - 1)
    else x
  in sum_aux 0 x;;

答案 2 :(得分:4)

某些函数语言(如Scheme)指定tail recursion 必须优化为等效于迭代;因此,Scheme中的尾递归函数永远不会导致堆栈溢出,无论它递归多少次(当然,假设它不会在除了末尾之外的其他地方递归或参与相互递归)。

大多数其他函数式语言不需要有效地实现尾递归;有些人选择这样做,有些则不这样做,但实施相对容易,所以我希望大多数实现都这样做。

答案 3 :(得分:4)

初学者很容易编写深度递归来破坏堆栈。目标Caml很不寻常,因为List函数对于长列表不是堆栈安全的。像Unison这样的应用程序实际上已经用一个堆栈安全版本替换了Caml标准List库。大多数其他实现在堆栈方面做得更好。 (免责声明:我的信息描述了Objective Caml 3.08;当前版本3.11可能更好。)

Standard ML of New Jersey是不寻常的,因为它不使用堆栈,所以你的深度递归一直持续到你用完堆。安德鲁·阿佩尔(Andrew Appel)出色的着作Compiling with Continuations中描述了这一点。

我认为这里不存在严重问题;它更像是一个“意识点”,如果你要编写很多递归代码,你更有可能在函数式语言中做,你必须知道非尾调用和堆栈大小为与您要处理的数据大小相比较。

答案 4 :(得分:0)

这很棘手 - 原则上是的,但是函数式语言的编译器和运行时在函数式语言中增加了递归程度。最基本的是大多数函数式语言运行时请求比正常迭代程序使用的堆栈大得多。但除此之外,由于语言的更严格限制,函数式语言编译器更能够将递归代码转换为非递归代码。