为什么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通常用goto
replace尾部位置的自recursion调用的方法在这种情况下不起作用。 CLR上的Scala可以像F#那样做。