在这个talk中,在前8分钟,Runar解释说Scala在消除尾部呼叫方面存在问题,这让我想知道F#是否有类似的问题?如果没有,为什么不呢?
答案 0 :(得分:21)
Scala中正确尾调用的问题是工程权衡之一。将SCC添加到Scala很容易:只需在SLS中添加一个句子即可。 Voilà:Scala中的PTC。从语言设计的角度来看,我们已经完成了。
现在糟糕的编译器编写者需要实现那个规范。好吧,使用PTC将编译成语言很容易......但不幸的是,JVM字节代码不是这样的语言。好的,那么GOTO
呢?不。延续?不。例外(已知等同于Continuations)?啊,现在我们到了某个地方!因此,我们可以使用异常来实现PTC。或者,或者,我们根本不能使用JVM调用堆栈并实现我们自己的堆栈。
毕竟,JVM上有多个Scheme实现,所有这些实现都支持PTC。这是一个神话,你不能在JVM上安装PTC,只是因为JVM不支持它们。毕竟,x86也没有它们,但是,仍然有x86上运行的语言有它们。
因此,如果可以在JVM上实现PTC,那么为什么Scala没有它们呢?就像我上面说的那样,你可以使用异常或你自己的堆栈来实现它们。但是使用控件流的异常或实现自己的堆栈意味着所有希望JVM调用堆栈以某种方式显示的东西将不再有效。
特别是,您将失去与Java工具生态系统(调试器,可视化工具,静态分析器)的所有互操作性。您还必须构建与Java库互操作的桥梁,这将很慢,因此您也会失去与Java库生态系统的互操作。
但这是Scala的主要设计目标!这就是Scala没有PTC的原因。
我称之为“Hickey's Theorem”,来自Clojure的设计师Rich Hickey,曾在一次谈话中说道:“Tail Calls,Interop,Performance - Pick Two。”
您还会向JIT编译器提供一些非常不寻常的字节代码模式,它们可能不知道如何优化。
如果您要将F#移植到JVM,您基本上必须做出这样的选择:您是否放弃了Tail Calls(您不能,因为它们是语言规范所要求的),您是否放弃了Interop还是你放弃表演?在.NET上,你可以拥有这三个,因为F#中的Tail Calls可以简单地编译成MSIL中的Tail Calls。 (虽然实际的翻译比这更复杂,但在某些极端情况下,MSIL中的Tail Calls的实现是错误的。)
这提出了一个问题:为什么不将尾调用添加到JVM?嗯,由于JVM字节代码中的设计缺陷,这非常困难。设计人员希望JVM字节代码具有某些安全属性。但是,而不是以这样的方式设计JVM字节代码语言,使得您无法首先编写不安全的程序(例如,在Java中,例如,您无法编写违反指针安全的程序,因为语言只是JVM字节代码本身并不安全,并且需要一个单独的字节码验证器才能使其安全。
该字节码验证器基于堆栈检查,Tail Calls更改堆栈。所以,这两个非常难以协调,但是如果没有字节码验证器,JVM就无法工作。花了很长时间和一些非常聪明的人才最终弄清楚如何在JVM上实现Tail Calls而不会丢失字节码验证器(参见A Tail-Recursive Machine with Stack Inspection by Clements and Felleisen和tail calls in the VM by John Rose (JVM lead designer)),所以我们现在已经从阶段,这是一个开放的研究问题到“只是”一个开放的工程问题的阶段。
请注意,Scala和其他一些语言 do 具有方法内直接尾递归。然而,这很无聊,实现方面:它只是一个while
循环。大多数目标都有while
个循环或类似的东西,例如JVM具有方法内GOTO
。 Scala也有scala.util.control.TailCalls
object,这是一种重要的蹦床。 (有关此概念的更一般版本,请参阅Stackless Scala With Free Monads by Rúnar Óli Bjarnason,这可以消除对堆栈的所有使用,而不仅仅是尾部调用。)这可以用于实现尾调用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,这也是一个明确的蹦床。
答案 1 :(得分:15)
F#尾部调用没有问题。这是它的作用:
如果你有一个尾递归函数,编译器会生成一个带有变异的循环,因为这比生成.tail
指令要快
在其他尾部调用位置(例如,当使用continuation或两个相互递归函数时),它会生成.tail
指令,因此尾部调用由CLR处理
默认情况下,在Visual Studio的调试模式下,尾部调用优化是关闭,因为这会使调试更容易(您可以检查堆栈),但您可以在如果需要,可以使用项目属性。
在过去,曾经有一些运行时(CLR x64和Mono)的.tail
指令存在问题,但所有这些都已修复,一切正常。
答案 2 :(得分:3)
事实证明,对于正确的尾调用,您必须以“释放模式”编译而不是默认的“调试模式”,或者打开项目属性,并在“生成”菜单中向下滚动并选中“生成尾调用“。感谢Arnavion在IRC.freenode.net #fsharp上的提示。