Lisp 尾部调用和尾部递归之间的区别是什么?

Lisp 尾部调用和尾部递归之间的区别是什么?,lisp,scheme,Lisp,Scheme,我知道尾部递归是函数对自身进行尾部调用的一种特殊情况。 但我不明白尾部调用和尾部递归有什么不同。 在实现了TCO(尾部调用优化)的“正确尾部递归”语言中,就像Scheme一样,这意味着尾部调用和尾部递归不会消耗堆栈或其他资源。 在编译器无法优化尾部递归的语言中,程序可能会耗尽堆栈并崩溃。 在“正确的尾部递归”语言中,为循环实现尾部递归的效率不亚于使用循环。让我们先消除“尾部调用”的歧义 尾部位置的调用是一个函数调用,其结果立即作为封闭函数的值返回。尾部位置是一个静态属性 在尾部位置的调用可以在不

我知道尾部递归是函数对自身进行尾部调用的一种特殊情况。 但我不明白尾部调用和尾部递归有什么不同。 在实现了TCO(尾部调用优化)的“正确尾部递归”语言中,就像Scheme一样,这意味着尾部调用和尾部递归不会消耗堆栈或其他资源。 在编译器无法优化尾部递归的语言中,程序可能会耗尽堆栈并崩溃。 在“正确的尾部递归”语言中,为循环实现尾部递归的效率不亚于使用循环。

让我们先消除“尾部调用”的歧义

尾部位置的调用是一个函数调用,其结果立即作为封闭函数的值返回。尾部位置是一个静态属性

在尾部位置的调用可以在不将任何内容推到堆栈上的情况下实现,因为旧的堆栈框架基本上是无用的(在函数式语言中通常是正确的,但在C等语言中不一定如此)。正如盖伊·斯蒂尔(Guy Steele)所说,尾部调用是传递参数的跳转

粗略地说,如果一个语言实现的渐近空间使用率与一个实现所有尾部位置的调用的渐近空间使用率相同,则该语言实现是正确的尾部递归的,因为它不需要堆栈增长。这是一个非常粗略的简化。如果你想了解完整的故事,请看克林格的

请注意,仅专门处理尾部递归函数不足以实现正确的尾部递归(任何尾部调用都必须专门处理)。这个术语有些误导


还要注意的是,还有其他方法可以在不实现尾部调用的情况下实现渐进空间效率。例如,您可以将它们实现为普通调用,然后通过删除无用的帧(以某种方式)定期压缩堆栈。正如你所说,尾部递归是尾部调用的一个特例。因此,任何实现一般TCO的语言都是“正确的尾部递归”

然而,相反的情况并不成立。有相当多的语言只优化尾部递归,因为这非常容易——您可以直接将其转换为循环,而不需要以新方式操纵堆栈的特定“尾部调用”操作。例如,这就是编译到JVM的语言(没有尾部调用指令)通常只优化尾部(自)递归的原因。(有一些技巧可以解决缺乏此类指导的问题,例如蹦床,但它们相当昂贵。)

完全尾部调用优化不仅适用于自(或相互)递归调用,而且适用于处于尾部位置的任何调用。特别是,它扩展到目标不是静态已知的调用,例如在调用一级函数或动态调度方法时!因此,它需要更复杂(尽管众所周知)的实现技术


许多函数式编程技术——还有一些流行的OO模式(例如or)——都需要完整的TCO才能真正可用。

好吧,这两者有某种关联−因为它们都有“尾巴”这个词− 但是它们是完全不同的

尾部递归是带有特定约束的递归,尾部调用是函数调用。你的问题有点像“动物和猫有什么区别?”

尾部调用是处于尾部位置的函数调用。 示例:
f(x)
中的
f(x)
,以及
g(★)
g(f(x))
反例:
f(x)
in
1+f(x)
和in
g(f(x))

尾部递归是一种递归,其中递归调用是尾部调用。 示例:
f(★)在“=”符号的右侧
f(x)=f(x)
f(x,y)=如果x==0,则y=f(x-1,y+x)
我已经定义了两个递归函数,它们通过尾部调用调用自己。就是这样


在具有TCO的语言中,尾部调用不需要任何成本,因此(尾部)递归在常量堆栈中工作,每个人都很高兴。

我不确定我是否理解这个问题。您自己不是已经解释了尾部调用和尾部递归之间的区别吗?正如您所说,尾部递归是指尾部调用调用函数本身,而只是“尾部调用”可以是对任何函数的调用。这就是区别。确实如此。简单吗?我怀疑可能有更大的区别,或者我错了?当相互递归函数相互调用时,相互递归也可以是尾部递归(不仅仅是每个函数本身)始终处于尾部位置。此外,这只是关于堆栈。其他资源可以被消耗,也可以不被消耗,TCO的特点是堆栈的持续使用。我梦想有一天人们将不再使用术语“尾部递归”,而只使用“尾部调用”.尾部调用消除的概念与递归无关。尾部调用消除只是对于尾部自调用最容易实现,但对于任何调用都有意义,正如Andreas所指出的。回答得好!我真的很想看一些关于你所说的确切含义的详细说明“仅仅处理尾部递归函数不足以实现正确的尾部递归”。这指的是什么?@Willenss,请参阅Andreas的答案,其中谈到了这一点,并提供了链接以了解更多细节。还有一件事。旧堆栈框架真的那么没用吗?如果“尾部调用是传递参数的跳转”“,它在哪里传递它们?在旧的堆栈框架上,不是吗?(至少当它调用自身时)。我认为TCO与直接跳转一样,都是关于堆栈框架的重用。比如说,三个相互递归的函数,这三个框架也可能在堆栈中共存。看起来TCO是关于恢复被抛弃的非结构化编程的GOTO:”@对于大多数尾部调用,调用方和被调用方是不同的,并且可能有不同大小和布局的堆栈帧