为什么Scala编译器不应用尾部调用优化,除非方法是最终的?

为什么Scala编译器不应用尾部调用优化,除非方法是最终的?

例如,这个:

class C { @tailrec def fact(n: Int, result: Int): Int = if(n == 0) result else fact(n - 1, n * result) } 

结果是

错误:无法优化@tailrec注释的方法:它既不是私有也不是最终的,因此可以被覆盖

如果编译器在这种情况下应用TCO ,究竟会出现什么问题呢?

考虑以下与REPL的交互。 首先我们用一个阶乘方法定义一个类:

 scala> class C { def fact(n: Int, result: Int): Int = if(n == 0) result else fact(n - 1, n * result) } defined class C scala> (new C).fact(5, 1) res11: Int = 120 

现在让我们在一个子类中覆盖它,以加倍超类的答案:

 scala> class C2 extends C { override def fact(n: Int, result: Int): Int = 2 * super.fact(n, result) } defined class C2 scala> (new C).fact(5, 1) res12: Int = 120 scala> (new C2).fact(5, 1) 

你最后一次打电话的结果是什么? 你可能会期待240.但是没有:

 scala> (new C2).fact(5, 1) res13: Int = 7680 

这是因为当超类的方法进行recursion调用时,recursion调用通过子类。

如果压倒性的工作使得240是正确的答案,那么在这里的超类中执行tail-call优化是安全的。 但是这不是Scala(或Java)的工作原理。

除非方法被标记为final, 否则在进行recursion调用时可能不会自行调用。

这就是为什么@tailrec不能工作,除非方法是最终的(或私人的)。

更新:我build议阅读另外两个答案(约翰和雷克斯的)以及。

recursion调用可能是一个子类而不是一个超类; final会阻止的。 但为什么你想要这样的行为呢? 斐波那契数列没有提供任何线索。 但是这样做:

 class Pretty { def recursivePrinter(a: Any): String = { a match { case xs: List[_] => xs.map(recursivePrinter).mkString("L[",",","]") case xs: Array[_] => xs.map(recursivePrinter).mkString("A[",",","]") case _ => a.toString }} } class Prettier extends Pretty { override def recursivePrinter(a: Any): String = { a match { case s: Set[_] => s.map(recursivePrinter).mkString("{",",","}") case _ => super.recursivePrinter(a) }} } scala> (new Prettier).recursivePrinter(Set(Set(0,1),1)) res8: String = {{0,1},1} 

如果Pretty调用是尾recursion的,那么我们会打印{Set(0, 1),1}因为扩展名不适用。

由于这种recursion非常有用,并且如果允许tail调用非final方法,它将被销毁,编译器会插入一个真正的调用。

foo::fact(n, res)表示你的例程。 让baz::fact(n, res)表示别人的例程覆盖。

编译器告诉你,语义允许baz::fact()是一个包装器,如果需要, 可以上调(?) foo::fact() 。 在这种情况下,规则是foo::fact() ,当它复发时,必须激活baz::fact()而不是foo::fact() ,而foo::fact()是尾recursion, baz::fact()可能不是。 在这一点上,而不是循环尾部recursion调用, foo::fact()必须返回到baz::fact() ,所以它可以放松自己。

如果编译器在这种情况下应用TCO,究竟会出现什么问题呢?

没有什么会出错的。 任何适当的尾巴呼叫消除的语言将这样做(SML,OCaml,F#,Haskell等)。 Scala没有的唯一原因是JVM不支持尾recursion,而Scala通常用gotoreplace尾部位置的自recursion调用的方法在这种情况下不起作用。 CLR上的Scala可以像F#那样做。