Scala:尾递归的跟踪调用及其局限

开发 后端
本文节选自Martin Odersky,Lex Spoon和Bill Venners所著,Regular翻译的《Programming in Scala》的第八章。Scala是一种针对 JVM 将函数和面向对象技术组合在一起的编程语言。

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

def approximate(guess: Double): Double =  
 if (isGoodEnough(guess)) guess  
 else approximate(improve(guess))  
  • 1.
  • 2.
  • 3.

51CTO编辑推荐:Scala编程语言专题

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

def approximateLoop(initialGuess: Double): Double = {  
 var guess = initialGuess  
 while (!isGoodEnough(guess))  
  guess = improve(guess)  
 guess  
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

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

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

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

跟踪尾递归函数

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

def boom(x: Int): Int =  
 if (x == 0throw new Exception("boom!")  
 else boom(x - 1) + 1 
  • 1.
  • 2.
  • 3.

这个函数不是尾递归,因为在递归调用之后执行了递增操作。如果执行它,你会得到预期的:

scala> boom(3)  
java.lang.Exception: boom!  
 at .boom(< console>:5)  
 at .boom(< console>:6)  
 at .boom(< console>:6)  
 at .boom(< console>:6)  
 at .< init>(< console>:6)  
...  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

def bang(x: Int): Int =  
 if (x == 0throw new Exception("bang!")  
 else bang(x 1)  
  • 1.
  • 2.
  • 3.

你会得到:

scala> bang(5)  
java.lang.Exception: bang!  
 at .bang(< console>:5)  
 at .< init>(< console>:6)  
...  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

-g:notailcalls 
  • 1.

把这个参数传给scala的shell或者scalac编译器。定义了这个选项,你就能得到一个长长的堆栈跟踪了:

scala> bang(5)  
java.lang.Exception: bang!  
 at .bang(< console>:5)  
 at .bang(< console>:5)  
 at .bang(< console>:5)  
 at .bang(< console>:5)  
 at .bang(< console>:5)  
 at .bang(< console>:5)  
 at .< init>(< console>:6)  
...  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

尾调用优化

approximate的编译后代码实质上与approximateLoop的编译后代码相同。两个函数编译后都是同样的事三个Java字节码指令。如果你看一下Scala编译器对尾递归方法,approximate,产生的字节码,你会看到尽管isGoodEnough和improve都被方法体调用,approximate却没有。Scala编译器优化了递归调用:

public double approximate(double);  
 Code:  
  0: aload_0  
  1: astore_3  
  2: aload_0  
  3: dload_1  
  4: invokevirtual #24//Method isGoodEnough:(D)Z  
  7: ifeq 12 
  10: dload_1  
  11: dreturn  
  12: aload_0  
  13: dload_1  
  14: invokevirtual #27//Method improve:(D)D  
  17: dstore_1  
  18goto 2 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

尾递归的局限

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

def isEven(x: Int): Boolean =  
 if (x == 0true else isOdd(x - 1)  
def isOdd(x: Int): Boolean =  
 if (x == 0false else isEven(x - 1)  
  • 1.
  • 2.
  • 3.
  • 4.

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

val funValue = nestedFun _  
def nestedFun(x: Int) {  
 if (x != 0) { println(x); funValue(x - 1) }  
}  
  • 1.
  • 2.
  • 3.
  • 4.

funValue变量指向一个实质是包装了nestedFun的调用的函数值。当你把这个函数值应用到参数上,它会转向把nestedFun应用到同一个参数,并返回结果。因此你或许希望Scala编译器能执行尾调用优化,但在这个例子里做不到。因此,尾调用优化受限于方法或嵌套函数在***一个操作调用本身,而没有转到某个函数值或什么其它的中间函数的情况。(如果你还不能完全明白尾递归,参见8.9节)。

【相关阅读】

  1. Scala允许的重复参数
  2. 学习Scala的闭包
  3. Scala的偏应用函数
  4. Scala:函数文本的短格式和占位符语法
  5. 介绍Scala的***类函数

责任编辑:book05 来源: Artima
相关推荐

2020-05-27 07:38:36

尾递归优化递归函数

2020-09-30 08:07:46

如何优化尾调用

2010-09-17 13:01:44

Python

2009-07-21 07:30:00

Scala程序Application

2017-07-12 10:00:22

深度学习小数据样本深度网络

2017-07-11 15:25:53

深度学习人工智能

2013-08-20 09:23:06

Scala递归

2024-03-12 09:43:45

2019-03-26 08:15:45

iOS尾调用Objective-C

2009-12-15 11:05:05

2009-11-27 16:20:22

PHP递归调用

2024-05-08 08:00:00

2011-04-25 16:35:06

Linux调用

2009-08-06 18:02:22

存储过程

2014-04-16 10:54:45

Javascript递归调用

2009-07-08 12:43:59

Scala ServlScala语言

2023-12-04 07:09:53

函数递归python

2020-10-31 17:33:18

Scala语言函数

2010-09-14 15:34:41

Scala

2021-03-24 10:00:32

Python递归函数Python基础
点赞
收藏

51CTO技术栈公众号