Scala学习(五)函数和闭包

第1章函数和闭包

当程序变得庞大时,你需要一些方法把它们分割成更小的,更易管理的片段。为了分割控制流,Scala提供了所有有经验的程序员都熟悉的方式:把代码分割成函数。实际上,Scala提供了许多Java中没有的定义函数的方式。除了作为对象成员函数的方法之外,还有内嵌在函数中的函数,函数文本和函数值。本章带你体会所有Scala中的这些函数的风味。

1.1 方法

定义函数最通用的方法是作为某个对象的成员。这种函数被称为方法:method。作为例子,代码8.1展示了两个可以合作根据一个给定的文件名读文件并打印输出所有长度超过给定宽度的行的方法。每个打印输出的行前缀它出现的文件名:

import scala.io.Source

object LongLines {

def processFile(filename: String, width: Int) {

val source = Source.fromFile(filename)

for (line <- source.getLines)

processLine(filename, width, line)

}

private def processLine(filename:String, width:Int, line:String) {

if (line.length > width)

println(filename+": "+line.trim)

}

}

代码 8.1 带私有的processLine方法的LongLines对象

processFile方法带了filename和width做参数。它用文件名创建了一个Source对象并,在for表达式的发生器中,对source调用getLines。第3章的第十二步曾经提到,getLines返回一个枚举器,能在每一次枚举中从文件里取出一行,包括换行符。for表达式通过调用帮助方法,processLine,处理所有的文件行。processLine方法带三个参数:filename,width和line。它检查是否文件行的长度超过给定长度,如果是,就打印文件名,跟着一个冒号,然后是文件行。

为了从命令行里使用LongLines,我们需要创建一个应用,把第一个命令行参数当作行长度,并把后续的参数解释为文件名:[1]

object FindLongLines {

def main(args: Array[String]) {

val width = args(0).toInt

for (arg <- args.drop(1))

LongLines.processFile(arg, width)

}

}

下面是如何使用这个应用程序去发现LongLines.scala里超过45字符长度的行(只有一行):

$ scala FindLongLines 45 LongLines.scala

LongLines.scala: def processFile(filename: String, width: Int) {

到此为止,这些与你能用面向对象语言做的很像。然而Scala里的函数概念比方法更宽泛。Scala描述函数的另外的方法将在后续节中描述。

1.2 本地函数

上节中processFile方法的建立演示了函数式编程风格的一个重要设计原则:程序应该被解构成若干小的函数,每个完成一个定义良好的任务。单个函数经常很小。这种风格的好处是它给了程序员许多可以灵活组装成更复杂事物的建造模块。每个小块应该充分简化到足以单独理解。

这种方式的一个问题是所有这些帮助函数的名称会污染程序的命名空间。在解释器里这不太成问题,但是一旦函数被打包成可复用的类和对象,就最好对类的客户隐藏帮助函数。它们经常不能独立表达什么意思,并且如果之后用其它方式重写类的话,也常会想保持能删掉帮助方法的足够的灵活度。

Java里,达成这个目的的主要工具是private方法。这种私有方法的方式在Scala里同样有效,如代码8.1里描述的,但是Scala提供了另一种方式:你可以把函数定义在另一个函数中。就好象本地变量那样,这种本地函数仅在包含它的代码块中可见。以下是一个例子:

def processFile(filename: String, width: Int) {

def processLine(filename:String, width:Int, line:String) {

if (line.length > width) print(filename+": "+line)

}

val source = Source.fromFile(filename)

for (line <- source.getLines) {

processLine(filename, width, line)

}

}

在这个例子中,我们通过把私有方法,processLine,转换为本地方法,processFile,重构了展示在代码8.1中原本的LongLines版本。为了做到这点我们去掉了private修饰符,它仅能应用于方法(并且仅被方法需要),然后把processLine的定义放在processFile的定义里。作为本地函数,processLine的范围局限于processFile之内,外部无法访问。

既然processLine被定义在processFile里,另一个改善变为可能了。请注意filename和width是怎样不改变地传入到帮助函数中。这不是必须的,因为本地函数可以访问包含它们的函数的参数。你可以直接使用外部processLine函数的参数,如代码8.2所示:

import scala.io.Source

object LongLines {

def processFile(filename: String, width: Int) {

def processLine(line: String) {

if (line.length > width)

print(filename +": "+ line)

}

val source = Source.fromFile(filename)

for (line <- source.getLines)

processLine(line)

}

}

代码 8.2 带本地processLine方法的LongLines

更简单了,不是吗?这种对外层函数的参数的使用是Scala提供的通用嵌套的很平常也很有用的例子。7.7节描述的嵌套和作用域应用于所有的Scala架构,包括函数。这是一个简单的原则,不过非常强大,尤其在拥有函数作为第一类值的语言中。

1.3 函数是第一类值

Scala拥有第一类函数:first-class function。你不仅可以定义函数和调用它们,还可以把函数写成没有名字的文本:literal并把它们像值:value那样传递。我们在第2章介绍了函数文本并在第42页的图2.2里展示了基本语法。

函数文本被编译进一个类,类在运行期实例化的时候是一个函数值:function value[2]因此函数文本和值的区别在于函数文本存在于源代码,而函数值存在于运行期对象。这个区别很像类(源代码)和对象(运行期)的那样。

以下是对数执行递增操作的函数文本的简单例子:

(x: Int) => x + 1

=>指明这个函数把左边的东西(任何整数x)转变成右边的东西(x + 1)。所以,这是一个把任何整数x映射为x + 1的函数。

函数值是对象,所以如果你愿意可以把它们存入变量。它们也是函数,所以你可以使用通常的括号函数调用写法调用它们。以下是这两种动作的例子:

scala> var increase = (x: Int) => x + 1

increase: (Int) => Int = <function>

scala> increase(10)

res0: Int = 11

本例中,因为increase是var,你可以在之后重新赋给它不同的函数值。

scala> increase = (x: Int) => x + 9999

increase: (Int) => Int = <function>

scala> increase(10)

res2: Int = 10009

如果你想在函数文本中包括超过一个语句,用大括号包住函数体,一行放一个语句,就组成了一个代码块。与方法一样,当函数值被调用时,所有的语句将被执行,而函数的返回值就是最后一行产生的那个表达式

scala> increase = (x: Int) => {

println("We")

println("are")

println("here!")

x + 1

}

increase: (Int) => Int = <function>

scala> increase(10)

We

are

here!

res4: Int = 11

于是现在你已经看到了有如螺丝和螺帽的函数文本和函数值。许多Scala库给你使用它们的机会。例如,所有的集合类都能用到foreach方法。[3]它带一个函数做参数,并对每个元素调用该函数。下面是如何用它打印输出所有列表元素的代码:

scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)

someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> someNumbers.foreach((x: Int) => println(x))

-11

-10

-5

0

5

10

另一个例子是,集合类型还有filter方法。这个方法选择集合类型里可以通过用户提供的测试的元素。测试是通过函数的使用来提供的。例如,函数(x: Int) => x > 0可以被用作过滤。这个函数映射正整数为真,其它为假。下面说明如何把它用作filter:

scala> someNumbers.filter((x: Int) => x > 0)

res6: List[Int] = List(5, 10)

像foreach和filter这样的方法将在本书后面描述。第16章讨论了它们在类List中的使用。第17章讨论了它们在其他集合类型中的使用。

1.4 函数文本的短格式

Scala提供了许多方法去除冗余信息并把函数文本写得更简短。注意留意这些机会,因为它们能让你去掉代码里乱七八糟的东西。

一种让函数文本更简短的方式是去除参数类型。因此,前面带过滤器的例子可以写成这样:

scala> someNumbers.filter((x) => x > 0)

res7: List[Int] = List(5, 10)

Scala编译器知道x一定是整数,因为它看到你立刻使用了这个函数过滤整数列表(由someNumbers暗示)。这被称为目标类型化:target typing,因为表达式的目标使用——本例中someNumbers.filter()的参数——影响了表达式的类型化——本例中决定了x参数的类型。目标类型化的精确细节并不重要。你可以简单地从编写一个不带参数类型的函数文本开始,并且,如果编译器不能识别,再加上类型。几次之后你就对什么情况编译器能或不能解开谜题有感觉了。

第二种去除无用字符的方式是省略类型是被推断的参数之外的括号。前面例子里,x两边的括号不是必须的:

scala> someNumbers.filter(x => x > 0)

res8: List[Int] = List(5, 10)

1.5 占位符语法

如果想让函数文本更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数文本内仅出现一次。比如,_ > 0对于检查值是否大于零的函数来说就是非常短的标注:

scala> someNumbers.filter(_ > 0)

res9: List[Int] = List(5, 10)

你可以把下划线看作表达式里需要被“填入”的“空白”。这个空白在每次函数被调用的时候用函数的参数填入。例如,由于someNumbers在第113页被初始化为值List(-11, -10, -5, 0, 5, 10),filter方法会把_ > 0里的空格首先用-11替换,就如-11 > 0,然后用-10替换,如-10 > 0,然后用-5,如-5 > 0,这样直到List的最后一个值。因此,函数文本_ > 0与稍微冗长一点儿的x => x > 0相同,演示如下:

scala> someNumbers.filter(x => x > 0)

res10: List[Int] = List(5, 10)

有时你把下划线当作参数的占位符时,编译器有可能没有足够的信息推断缺失的参数类型。例如,假设你只是写_ + _:

scala> val f = _ + _

<console>:4: error: missing parameter type for expanded

function ((x$1, x$2) => x$1.$plus(x$2))

val f = _ + _

ˆ

这种情况下,你可以使用冒号指定类型,如下:

scala> val f = (_: Int) + (_: Int)

f: (Int, Int) => Int = <function>

scala> f(5, 10)

res11: Int = 15

请注意_ + _将扩展成带两个参数的函数文本。这也是仅当每个参数在函数文本中最多出现一次的情况下你才能使用这种短格式的原因。多个下划线指代多个参数,而不是单个参数的重复使用。第一个下划线代表第一个参数,第二个下划线代表第二个,第三个……,如此类推。

1.6 偏应用函数

尽管前面的例子里下划线替代的只是单个参数,你还可以使用一个下划线替换整个参数列表。例如,写成println(_),或者更好的方法你还可以写成println _。下面是一个例子:

someNumbers.foreach(println _)

Scala把这种短格式直接看作是你输入了下列代码:

someNumbers.foreach(x => println(x))

因此,这个例子中的下划线不是单个参数的占位符。它是整个参数列表的占位符。请记住要在函数名和下划线之间留一个空格,因为不这样做编译器会认为你是在说明一个不同的符号,比方说是,似乎不存在的名为println_的方法。

以这种方式使用下划线时,你就正在写一个偏应用函数:partially applied function。Scala里,当你调用函数,传入任何需要的参数,你就是在把函数应用到参数上。如,给定下列函数:

scala> def sum(a: Int, b: Int, c: Int) = a + b + c

sum: (Int,Int,Int)Int

你就可以把函数sum应用到参数1,2和3上,如下:

scala> sum(1, 2, 3)

res12: Int = 6

偏应用函数是一种表达式,你不需要提供函数需要的所有参数。代之以仅提供部分,或不提供所需参数。比如,要创建不提供任何三个所需参数的调用sum的偏应用表达式,只要在“sum”之后放一个下划线即可。然后可以把得到的函数存入变量。举例如下:

scala> val a = sum _

a: (Int, Int, Int) => Int = <function>

有了这个代码,Scala编译器以偏应用函数表达式,sum _,实例化一个带三个缺失整数参数的函数值,并把这个新的函数值的索引赋给变量a。当你把这个新函数值应用于三个参数之上时,它就转回头调用sum,并传入这三个参数

scala> a(1, 2, 3)

res13: Int = 6

实际发生的事情是这样的:名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照偏应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带三个参数。[4]之所以带三个参数是因为sum _表达式缺少的参数数量为三。Scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用,传入三个参数1,2,3。因此a(1,2,3)是下列代码的短格式:

scala> a.apply(1, 2, 3)

res14: Int = 6

Scala编译器根据表达式sum _自动产生的类里的apply方法,简单地把这三个缺失的参数前转到sum,并返回结果。本例中apply调用了sum(1,2,3),并返回sum返回的,6。

这种一个下划线代表全部参数列表的表达式的另一种用途,就是把它当作转换def为函数值的方式。例如,如果你有一个本地函数,如sum(a: Int, b: Int, c: Int): Int,你可以把它“包装”在apply方法具有同样的参数列表和结果类型的函数值中。当你把这个函数值应用到某些参数上时,它依次把sum应用到同样的参数,并返回结果。尽管不能把方法或嵌套函数赋值给变量,或当作参数传递给其它方法,但是如果你把方法或嵌套函数通过在名称后面加一个下划线的方式包装在函数值中,就可以做到了。

现在,尽管sum _确实是一个偏应用函数,或许对你来说为什么这么称呼并不是很明显。这个名字源自于函数未被应用于它所有的参数。在sum _的例子里,它没有应用于任何参数。不过还可以通过提供某些但不是全部需要的参数表达一个偏应用函数。举例如下:

scala> val b = sum(1, _: Int, 3)

b: (Int) => Int = <function>

这个例子里,你提供了第一个和最后一个参数给sum,但中间参数缺失。因为仅有一个参数缺失,Scala编译器会产生一个新的函数类,其apply方法带一个参数。在使用一个参数调用的时候,这个产生的函数的apply方法调用sum,传入1,传递给函数的参数,还有3。如下:

scala> b(2)

res15: Int = 6

这个例子里,b.apply调用了sum(1,2,3)。

scala> b(5)

res16: Int = 9

这个例子里,b.apply调用了sum(1,5,3)。

如果你正在写一个省略所有参数的偏应用程序表达式,如println _或sum _,而且在代码的那个地方正需要一个函数,你可以去掉下划线从而表达得更简明。例如,代之以打印输出someNumbers里的每一个数字(定义在第113页)的这种写法:

someNumbers.foreach(println _)

你可以只是写成:

someNumbers.foreach(println)

最后一种格式仅在需要写函数的地方,如例子中的foreach调用,才能使用。编译器知道这种情况需要一个函数,因为foreach需要一个函数作为参数传入。在不需要函数的情况下,尝试使用这种格式将引发一个编译错误。举例如下:

scala> val c = sum

<console>:5: error: missing arguments for method sum...

follow this method with `_' if you want to treat it as

a partially applied function

val c = sum

ˆ

scala> val d = sum _

d: (Int, Int, Int) => Int = <function>

scala> d(10, 20, 30)

res17: Int = 60

为什么要使用尾下划线?

Scala的偏应用函数语法凸显了Scala与经典函数式语言如Haskell或ML之间,设计折中的差异。在经典函数式语言中,偏应用函数被当作普通的例子。更进一步,这些语言拥有非常严格的静态类型系统能够暴露出你在偏应用中可能犯的所有错误。Scala与指令式语言如Java关系近得多,在这些语言中没有应用所有参数的方法会被认为是错误的。进一步说,子类型推断的面向对象的传统和全局的根类型接受一些被经典函数式语言认为是错误的程序。

举例来说,如果你误以为List的drop(n: Int)方法如tail(),那么你会忘记你需要传递给drop一个数字。你或许会写,“println(drop)”。如果Scala采用偏应用函数在哪儿都OK的经典函数式传统,这个代码就将通过类型检查。然而,你会惊奇地发现这个println语句打印的输出将总是<function>!可能发生的事情是表达式drop将被看作是函数对象。因为println可以带任何类型对象,这个代码可以编译通过,但产生出乎意料的结果。

为了避免这样的情况,Scala需要你指定显示省略的函数参数,尽管标志简单到仅用一个‘_’。Scala允许你仅在需要函数类型的地方才能省略这个仅用的_。

1.7 闭包

到本章这里,所有函数文本的例子仅参考了传入的参数。例如,(x: Int) => x > 0里,函数体用到的唯一变量,x > 0,是x,被定义为函数参数。然而也可以参考定义在其它地方的变量:

(x: Int) => x + more // more是多少?

函数把“more”加入参考,但什么是more呢?从这个函数的视点来看,more是个自由变量:free variable,因为函数文本自身没有给出其含义。相对的,x变量是一个绑定变量:bound variable,因为它在函数的上下文中有明确意义:被定义为函数的唯一参数,一个Int。如果你尝试独立使用这个函数文本,范围内没有任何more的定义,编译器会报错说:

scala> (x: Int) => x + more

<console>:5: error: not found: value more

(x: Int) => x + more

ˆ

另一方面,只要有一个叫做more的什么东西同样的函数文本将工作正常:

scala> var more = 1

more: Int = 1

scala> val addMore = (x: Int) => x + more

addMore: (Int) => Int = <function>

scala> addMore(10)

res19: Int = 11

依照这个函数文本在运行时创建的函数值(对象)被称为闭包:closure。名称源自于通过“捕获”自由变量的绑定对函数文本执行的“关闭”行动。不带自由变量的函数文本,如(x: Int) => x + 1,被称为封闭术语:closed term,这里术语:term指的是一小部分源代码。因此依照这个函数文本在运行时创建的函数值严格意义上来讲就不是闭包,因为(x: Int) => x + 1在编写的时候就已经封闭了。但任何带有自由变量的函数文本,如(x: Int) => x + more,都是开放术语:open term。因此,任何依照(x: Int) => x + more在运行期创建的函数值将必须捕获它的自由变量,more,的绑定。由于函数值是关闭这个开放术语(x: Int) => x + more的行动的最终产物,得到的函数值将包含一个指向捕获的more变量的参考,因此被称为闭包

这个例子带来一个问题:如果more在闭包创建之后被改变了会发生什么事?Scala里,答案是闭包看到了这个变化。如下:

scala> more = 9999

more: Int = 9999

scala> addMore(10)

res21: Int = 10009

直觉上,Scala的闭包捕获了变量本身,而不是变量指向的值。[5]就像前面演示的例子,依照(x: Int) => x + more创建的闭包看到了闭包之外做出的对more的变化。反过来也同样。闭包对捕获变量作出的改变在闭包之外也可见。下面是一个例子:

scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)

someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> var sum = 0

sum: Int = 0

scala> someNumbers.foreach(sum += _)

scala> sum

res23: Int = -11

例子用了一个循环的方式计算List的累加和。变量sum处于函数文本sum += _的外围,函数文本把数累加到sum上。尽管这是一个在运行期改变sum的闭包,作为结果的累加值,-11,仍然在闭包之外可见

如果闭包访问了某些在程序运行时有若干不同备份的变量会怎样?例如,如果闭包使用了某个函数的本地变量,并且函数被调用很多次会怎样?每一次访问使用的是变量的哪个实例?

仅有一个答案与语言余下的部分共存:使用的实例是那个在闭包被创建的时候活跃的。例如,以下是创建和返回“递增”闭包的函数:

def makeIncreaser(more: Int) = (x: Int) => x + more

每次函数被调用时都会创建一个新闭包。每个闭包都会访问闭包创建时活跃的more变量

scala> val inc1 = makeIncreaser(1)

inc1: (Int) => Int = <function>

scala> val inc9999 = makeIncreaser(9999)

inc9999: (Int) => Int = <function>

调用makeIncreaser(1)时,捕获值1当作more的绑定的闭包被创建并返回。相似地,调用makeIncreaser(9999),捕获值9999当作more的闭包被返回。当你把这些闭包应用到参数上(本例中,只有一个参数,x,必须被传入),回来的结果依赖于闭包被创建时more是如何定义的:

scala> inc1(10)

res24: Int = 11

scala> inc9999(10)

res25: Int = 10009

尽管本例中more是一个已经返回的方法调用的参数也没有区别。Scala编译器在这种情况下重新安排了它以使得捕获的参数继续存在于堆中,而不是堆栈中,因此可以保留在创建它的方法调用之外。这种重新安排的工作都是自动关照的,因此你不需要操心。请任意捕获你想要的变量:val,var,或参数。

1.8 重复参数

Scala允许你指明函数的最后一个参数可以是重复的。这可以允许客户向函数传入可变长度参数列表。想要标注一个重复参数,在参数的类型之后放一个星号。例如:

scala> def echo(args: String*) =

for (arg <- args) println(arg)

echo: (String*)Unit

这样定义,echo可以被零个至多个String参数调用:

scala> echo()

scala> echo("one")

one

scala> echo("hello", "world!")

hello

world!

函数内部,重复参数的类型是声明参数类型的数组。因此,echo函数里被声明为类型“String*”的args的类型实际上是Array[String]。然而,如果你有一个合适类型的数组,并尝试把它当作重复参数传入,你会得到一个编译器错误:

scala> val arr = Array("What's", "up", "doc?")

arr: Array[java.lang.String] = Array(What's, up, doc?)

scala> echo(arr)

<console>:7: error: type mismatch;

found : Array[java.lang.String]

required: String

echo(arr)

ˆ

要实现这个做法,你需要在数组参数后添加一个冒号和一个_*符号,像这样:

scala> echo(arr: _*)

What's

up

doc?

这个标注告诉编译器把arr的每个元素当作参数,而不是当作单一的参数传给echo。

1.9 尾递归

在7.2节中,我们提到过想要把更新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

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

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)

...

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

def bang(x: Int): Int =

if (x == 0) throw new Exception("bang!")

else bang(x 1)

你会得到:

scala> bang(5)

java.lang.Exception: bang!

at .bang(<console>:5)

at .<init>(<console>:6)

...

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

-g:notailcalls

把这个参数传给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)

...

尾调用优化

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

18: goto 2

尾递归的局限

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 funValue = nestedFun _

def nestedFun(x: Int) {

if (x != 0) { println(x); funValue(x - 1) }

}

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


[1] 本书中,我们通常不会在例子程序中检查命令行参数的合法性,这既是为了保护林木资源,也是为了减少会模糊例子重点部分的套路代码。作为交换就是,当输入错误时,代之以产生有助的错误信息,我们的例子程序将抛出异常。

[2] 任何函数值都是某个扩展了若干scala包的FunctionN特质之一的类的实例,如Function0是没有参数的函数,Function1是有一个参数的函数等等。每个FunctionN特质有一个apply方法用来调用函数。

[3] foreach方法被定义在特质Iterable中,它是List,Set,Array,还有Map的共有超特质。参见第17章相关细节。

[4] 产生的类扩展了特质Function3,定义了三个参数的apply方法。

[5] 相对的,Java的内部类根本不允许你访问外围范围内可以改变的变量,因此到底是捕获了变量还是捕获了它当前具有的值就没有差别了。

转载于:https://www.cnblogs.com/cxccbv/archive/2009/07/16/1524814.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值