函数和闭包之尾递归

前面提到过,如果想把更新var的while循环转换成仅使用val这种更函数式的风格的话,有时候你可以使用递归。下面的例子是通过不断改善猜测数字来逼近一个值的递归函数:

def approximate(guess:Double):Double =
    if(isGoodEnough(guess)) guess else approximate(improve(guess))

这样的函数,带合适的isGoodEnough和improve的实现,经常用在查找问题中。如果想要approximate函数执行得更快,你或许会 被诱惑 使用while循环编写以尝试加快它的速度,如:

def approximateLoop(initialGuess:Double):Double = {
    var guess = initialGuess
    while(!isGoodEnough(guess)) guess = improve(guess)
    guess
}

两种approximate版本哪个更好?就简洁性和避免var而言,第一个,函数式的胜出。但指令式的方式是否更有效率呢?实际上,如果我们测量执行的时间,就会发现它们几乎完全相同!这可能很令人惊奇,因为相比简单地从循环结尾跳到开头而言,递归调用看上去要更花时间

然而,在上面approximate的例子里,scala编译器可以应用一个重要的优化。注意递归调用是approximate函数体执行的最后一件事。像approximate这样,在它们最后一个动作调用自己的函数,被称为尾递归,tail recursive。scala编译器检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的跳转

事实上你不必刻意回避使用递归算法去解决问题。递归经常是比基于循环的更优美和简明的方案。如果方案是尾递归,就无须付出任何运行期开销


尾递归函数追踪

尾递归函数将不会为每个调用制造新的堆栈结构所有的调用将在一个结构内执行。这可能会让检查程序堆栈跟踪信息并失败的程序员感到惊奇。例如,这个函数调用自身若干次之后抛出了异常:

def boom(x:Int):Int = {
    if(x == 0) throw new Exception("boom!")
    else boom(x - 1) + 1
}

这个函数不是尾递归因为在递归调用之后执行了递增操作。如果执行:

boom(3)

将会得到预期的:

函数和闭包之尾递归_第1张图片

如果你现在修改了boom从而让它变成尾递归

def bang(x:Int):Int = 
    if(x == 0) throw new Exception("bang!")
    else bang(x - 1)

执行:

bang(5)

你会得到:

函数和闭包之尾递归_第2张图片

这回,你仅看到了bang的一个堆栈结构。或许你会认为bang在调用自己之前就崩溃了,但事实并非如此。如果你认为你会在看到椎栈跟踪时被尾调用优化搞糊涂,你可以用开关项关掉它-g:notailcalls

把这个参数传给scala的shell或者scalac编译器。定义了这个选项,你就能看到一个长长的堆栈跟踪。例如,我们在scala的shell中输入:

D:\> scala -g:notailcalls

然后输入:

def bang(x:Int):Int = 
    if(x == 0) throw new Exception("bang!")
    else bang(x - 1)

bang(5)

得到结果:

函数和闭包之尾递归_第3张图片

这就是放弃尾递归调用优化后的结果。


尾递归的局限

scala里尾递归的使用局限很大,因为JVM指令集使实现更加先进的尾递归形式变得很困难。scala仅优化了直接递归调用使其返回同一个函数。如果递归是间接的,就像在下面的例子里两个互相递归的函数,就没有优化的可能性了:

def isEven(x:Int):Boolean = 
    if(x == 0) true else isOdd(x - 1)
def isOdd(x:Int):Boolean = 
    if(x == 0) false else isEven(x - 1)

同样,如果最后一个调用是一个函数值也不能获得尾调用优化。请考虑下列递归代码的实现:

val fuValue = nestedFun _
def nestedFun(x:Int) {
    if(x != 0) {
        println(x)
        funValue(x - 1)
    }
}

funValue变量指向一个实质是包装了nestedFun的调用的函数值。当你把这个函数值应用到参数上,它会转向把nestedFun应用到同一个参数,并返回结果。因此你或许希望scala编译器能执行尾调用优化,但在这个例子里做不到。因此,尾调用优化限定了方法或嵌套函数必须在最后一个操作调用本身,而不是转到某个函数值或什么其他的中间函数的情况。

你可能感兴趣的:(scala)