Scheme中的递归和调用堆栈
Posted
技术标签:
【中文标题】Scheme中的递归和调用堆栈【英文标题】:Recursion in Scheme and the call-stack 【发布时间】:2014-02-14 19:30:25 【问题描述】:我是一名大学生,正在学习 Racket/Scheme 和 C 作为我的 CS 学位的入门课程。
我在网上读到,在 C 中使用迭代而不是递归通常是最佳实践,因为递归是昂贵的,因为将堆栈帧保存到调用堆栈等...
现在在像 Scheme 这样的函数式语言中,一直在使用递归。我知道尾递归在 Scheme 中是一个巨大的好处,据我了解,它只需要一个堆栈帧(有人可以澄清这一点吗?)无论递归有多深。
我的问题是:非尾递归呢?每个函数应用程序是否都保存在调用堆栈中?如果我能简要了解它是如何工作的,或者向我指出一个资源,我将不胜感激;我似乎在任何地方都找不到明确说明这一点的地方。
【问题讨论】:
【参考方案1】:是的,非尾部位置的调用需要向堆栈添加一些内容,以便它知道在调用返回时如何恢复工作。 (有关堆栈、尾调用和非尾调用的更详尽解释,请参阅 Steele 的论文揭穿“昂贵的过程调用”神话,或,被认为有害的过程调用实现,或 Lambda:终极 GOTO em> 链接自lambda papers page at readscheme.org。)
但是 Racket(以及许多其他方案和其他一些语言)实现了“堆栈”,因此即使您有深度递归,也不会耗尽堆栈空间。换句话说,Racket 没有堆栈溢出。这样做的一个原因是支持深度递归的技术与支持第一类延续的技术相一致,这也是 Scheme 标准所要求的。您可以在 Clinger 等人的 Implementation Strategies for First-Class Continuations 中了解它们。
【讨论】:
我不知道 Racket 不能有堆栈溢出...谢谢!我会阅读你本周末提供的一些论文。【参考方案2】:Scheme 要求消除尾调用。不是尾调用递归的代码将需要额外的堆栈帧。
暂时让我们假设 javascript 支持尾调用优化,这些函数定义中的第二个将仅使用 1 个堆栈帧,而第一个,由于 +
将需要一个额外的堆栈帧。
function sum(n)
if (n === 0)
return n;
return n + sum(n - 1);
function sum(n)
function doSum(total, n)
if (n === 0)
return total;
return doSum(total + n, n - 1);
return doSum(0, n);
通过将计算结果放在堆栈上,可以编写许多递归函数来优化尾调用
第一个定义的概念调用如下所示
3 + 总和(2) 3 + sum(2) = 3 + 2 + sum(1) 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) = 3 + 2 + 1 + 0 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) = 6 3 + sum(2) = 3 + 2 + sum(1) = 6 3 + 总和(2) = 6 6第二个定义的调用如下所示
总和(3,总和(2))=总和(5,总和(1))=总和(6,总和(0))=6【讨论】:
当你说:“而第一个,由于 + 将需要一个额外的堆栈帧”,任何时候都只会有一个额外的堆栈帧?我相信 C 会为每个函数调用添加一个新的堆栈框架,那么这是否意味着 Scheme 中的递归以这种方式更有效?如果我遗漏了什么,请告诉我;这只是一个奇怪的想法,直到明年我才能参加任何深入研究这个的课程。顺便感谢您的精彩回答。 尾调用优化是编译器的魔法。 Scheme 标准要求编译器实现它。使用跳转指令而不是调用来实现尾调用优化。因此,尾调用优化递归效率更高。 啊,所以如果没有尾递归(比如在第一个函数中),那么Scheme中递归的效率在C中是一样的吗? 还有更多细节,解释了Scheme,编译了C。假设所有条件都相同,跳转指令将比函数调用更快,或者没有尾 TCO 的递归会更糟。正在编译的 GCC 可能实现了比 Scheme 解释器更快的非 TCO 递归。 @Bhaskar Scheme 不需要解释,并且存在多个编译器。以上是关于Scheme中的递归和调用堆栈的主要内容,如果未能解决你的问题,请参考以下文章