Scala 中正确尾调用的问题是工程权衡之一。将 PTC 添加到 Scala 很容易:只需在 SLS 中添加一个句子。瞧:Scala 中的 PTC。从语言设计的角度来看,我们已经完成了。
现在可怜的编译器编写者需要实施该规范。好吧,用 PTC 将编译成语言很容易……但不幸的是,JVM 字节码不是这样的语言。好的,那么GOTO 呢?没有。续集?没有。异常(已知等同于延续)?啊,现在我们到了某个地方!因此,我们可以使用异常来实现 PTC。或者,我们也可以根本不使用 JVM 调用堆栈,而是实现我们自己的堆栈。
毕竟 JVM 上有多个 Scheme 实现,它们都支持 PTC 就好了。您不能在 JVM 上安装 PTC,只是因为 JVM 不支持它们,这是一个神话。毕竟,x86 也没有它们,但是,在 x86 上运行的语言有它们。
那么,如果在 JVM 上实现 PTC 是可能的,那么为什么 Scala 没有它们呢?就像我上面说的,您可以使用异常或您自己的堆栈来实现它们。但是对控制流使用异常或实现自己的堆栈意味着期望 JVM 调用堆栈以某种方式显示的所有内容都将不再有效。
特别是,您将失去与 Java 工具生态系统(调试器、可视化器、静态分析器)的几乎所有互操作性。您还必须建立与 Java 库互操作的桥梁,这会很慢,因此您也失去了与 Java 库生态系统的互操作性。
但这是 Scala 的主要设计目标!这就是 Scala 没有 PTC 的原因。
我将其称为“Hickey 定理”,以 Clojure 的设计师 Rich Hickey 的名字命名,他曾在一次演讲中说过“尾调用、互操作、性能 – 选择两个”。
您还会向 JIT 编译器展示一些非常不寻常的字节码模式,它可能不知道如何优化。
如果您要将 F# 移植到 JVM,您基本上必须做出正确的选择:您是否放弃尾调用(您不能,因为语言规范要求它们),您是否放弃 Interop还是你放弃性能?在 .NET 上,您可以同时拥有这三个,因为 F# 中的尾调用可以简单地编译为 MSIL 中的尾调用。 (虽然实际的翻译比这更复杂,而且在 MSIL 中尾调用的实现在某些极端情况下是错误的。)
这就提出了一个问题:为什么不向 JVM 添加尾调用?好吧,由于 JVM 字节码中的设计缺陷,这非常困难。设计者希望 JVM 字节码具有一定的安全属性。但是,不要以这样一种方式设计 JVM 字节码语言,即您一开始就不能编写不安全的程序(例如,在 Java 中,您不能编写违反指针安全的程序,因为该语言只是首先不让你访问指针),JVM 字节码本身是不安全的,需要一个单独的字节码验证器来保证它的安全。
该字节码验证器基于堆栈检查,并且尾调用更改堆栈。因此,两者非常难以协调,但 JVM 根本无法在没有字节码验证器的情况下工作。花了很长时间和一些非常聪明的人终于弄清楚如何在不丢失字节码验证器的情况下在 JVM 上实现尾调用(参见 A Tail-Recursive Machine with Stack Inspection by Clements and Felleisen 和 tail calls in the VM by John Rose (JVM lead designer)),所以我们现在已经从原来的阶段转移了一个开放的研究问题到它“只是”一个开放的工程问题的阶段。
请注意,Scala 和其他一些语言确实具有方法内直接尾递归。然而,这在实现方面非常无聊:它只是一个while 循环。大多数目标都有while 循环或类似的东西,例如JVM 有内部方法GOTO。 Scala 也有scala.util.control.TailCalls object,这是一种重新定义的蹦床。 (请参阅Stackless Scala With Free Monads by Rúnar Óli Bjarnason 了解这个想法的更通用版本,它可以消除 all 对堆栈的使用,而不仅仅是在尾调用中。)这可以用于实现尾调用算法Scala,但这与 JVM 堆栈不兼容,即它看起来不像是对其他语言或调试器的递归方法调用:
import scala.util.control.TailCalls._
def isEven(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(true) else tailcall(isOdd(xs.tail))
def isOdd(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(false) else tailcall(isEven(xs.tail))
isEven((1 to 100000).toList).result
def fib(n: Int): TailRec[Int] =
if (n < 2) done(n) else for {
x <- tailcall(fib(n - 1))
y <- tailcall(fib(n - 2))
} yield (x + y)
fib(40).result
Clojure 有 recur special form,它也是一个显式蹦床。