尾调用
使用循环方式
def factorial(n: Int): Int = {
def go(n: Int, acc: Int): Int = {
if (n <= 0) acc
else go(n - 1, n * acc)
}
go(n, 1)
}
想不通过修改一个循环变量而实现循环功能,可以借助递归函数。我们在阶乘函数内部定义了一个辅助的递归函数。
这种辅助函数习惯上被称为go或loop。在Scala中函数可以定义在任何代码块中,包括在另一个函数内部。就像一个局部变量,go函数只能被factorial函数内部引用。factorial函数的定义最终不过只是以循环的初始条件不断地调用go。
传给go的参数是循环的状态。子啊这个例子中包含n和一个当前阶乘的累计值acc。为进行下一次迭代,只需用新的循环转态递归调用go函数(这里是go(n - 1, n * acc)),要退出循环,返回一个不继续进行递归调用的值(这里是n <= 0)。Scala会检测到这种自递归,只要递归调用发生在尾部,编译器优化成类似while循环的字节码。
尾调用消除
我们说的尾调用是指调用者在一个递归调用之后不做任何事,只是返回这个调用结果。另一种情况:1 + go(n-1, n*acc),这里go不再是尾部,因为这个方法的结果还要参与其他运算。
如果递归调用实在一个函数的尾部位置,Scala会自动把递归编译为循环迭代,这样不会每次都进行栈的操作。
默认情况下Scala不会告诉你尾调用是否消除成功,可以通过tailrec注释来告诉编译器,如果编译不能消除尾部调用会给出编译错误。
import scala.annotation.tailrec
object Tailrec{
def factorial(n: Int): Int = {
@tailrec
def go(n: Int, acc: Int): Int = {
if (n <= 0) acc
else go(n - 1, n * acc)
}
go(n, 1)
}
}