当程序变得庞大时,你需要一些方法把它们分割成更小的,更易管理的片段。为了分割控制流,Scala
提供了所有有经验的程序员都熟悉的方式:把代码分割成函数。
方法:
定义函数最通用的方法是 作为某个对象的成员。这种函数被称为方法:method。
import scala.io.Source
object LongLines {
def
processFile(filename: String, width: Int) {
val source =
Source.fromFile(filename)
for (line
processLine(filename,
width, line)
}
private def
processLine(filename:String, width:Int, line:String) {
if (line.length
> width)
println(filename+":
"+line.trim)
}
}
本地函数:
本地函数:就是函数中定义函数。
函数式编程风格的一个重要设计原则:程序应该被解构成若干小的函数,每个完成一个定义良好的任务。单个函数经常很小。这种风格的好处是它给了程序员许多可以灵活组装成更复杂事物的建造模块。每个小块应该充分简化到足以单独理解。
这种方式的一个问题是所有这些帮助函数的名称会污染程序的命名空间。在解释器里这不太成问题,但是一旦函数被打包成可复用的类和对象,就最好对类的客户隐藏帮助函数。它们经常不能独立表达什么意思,并且如果之后用其它方式重写类的话,也常会想保持能删掉帮助方法的足够的灵活度。
java中主要是使用private方法。私有方法在Scala里同样有效,不过
Scala
提供了另一种方式:你可以把函数定义在另一个函数中。就好象本地变量那样,这种本地函数仅在包含它的代码块中可见。
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
processLine(line)
}
}
函数是第一类值:
Scala拥有第一类函数:first-class
funciton。你不仅可以定义函数和调用它们,还可以把函数写成没有名字的文本:literal并把它们像
值:value那样传递.
函数文本被编译进一个类,类在运行期实例化的时候是一个函数值:function
value。因此函数文本和值的区别在于
函数文本存在于源代码,而函数值存在于运行期对象。
scala> var increase = (x: Int)
=> x + 1
increase: (Int) => Int =
scala> increase(10)
res0: Int = 11
如果你想在函数文本中包括超过一个语句,用大括号包住函数体,一行放一个语句,就组成了一个代码块。与方法一样,当函数值被调用时,所有的语句将被执行,而函数的返回值就是最后一行产生的那个表达式。
scala> increase = (x: Int) =>
{
println("We")
println("are")
println("here!")
x + 1
}
increase: (Int) => Int =
scala> increase(10)
We
are
here!
res4: Int = 11
val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers.foreach((x: Int) =>
println(x))
someNumbers.filter((x: Int) => x
> 0)
foreach 方法被定义在特质 Iterable 中,它是 List,Set,Array,还有 Map
的共有超特质。
函数文本的短格式:
Scala 提供了许多方法去除冗余信息并把函数文本写得更简短。
someNumbers.filter(x => x >
0)
Scala编译器知道x一定是整数,因为它看到你立刻使用了这个函数过滤整数列表( 由someNumbers
暗示)。这被称为目标类型化:target
typing,因为表达式的目标使用——本例中someNumbers.filter()的参数——影响了表达式的类型化——本例中决定了
x 参数的类型。目标类型化的精确细节并不重要。
占位符语法:
如果想让函数文本更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数文本内仅出现一次。
scala> someNumbers.filter(_ >
0)
res9: List[Int] = List(5, 10)
你可以把下划线看作表达式里需要被“填入”的“空白”。这个空白在每次函数被调用的时候用函数的参数填入。
有时你把下划线当作参数的占位符时,编译器有可能没有足够的信息推断缺失的参数类型。
例如,假设你只是写_ + _:
scala> val f = _ + _
: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 =
请注意_ + _
将扩展成带两个参数的函数文本。这也是仅当每个参数在函数文本中最多出现一次的情况下你才能使用这种短格式的原因。多个下划线指代多个参数,而不是单个参数的重复使用。第一个下划线代表第一个参数,第二个下划线代表第二个,第三个......,如此类推。
偏应用函数:
尽管前面的例子里下划线替代的只是单个参数,你还可以使用一个下划线替换整个参数列表。
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
的偏应用表达式,只要在“sum”之后放一个下划线即可。然后可以把得到的函数存入变量。
scala> val a = sum _
a: (Int, Int, Int) => Int =
Scala 编译器以偏应用函数表达式,sum _,实例化一个带三个缺失整数参数的函数值,并把这个新的函数值的索引赋给变量 a
。
当你把这个新函数值应用于三个参数之上时,它就转回头调用 sum,并传入这三个参数:
scala> a(1, 2, 3)
res13: Int = 6
实际发生的事情是这样的:名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照偏应用函数表达式sum
_,自动产生的类的一个实例。编译器产生的类有一个apply方法带三个参数。之所以带三个参数是因为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 。
在 sum _
的例子里,它没有应用于任何参数。不过还可以通过提供某些但不是全部需要的参数表达一个偏应用函数。举例如下:
scala> val b = sum(1, _: Int, 3)
b: (Int) => Int =
scala> b(2)
res15: Int = 6
如果你正在写一个省略所有参数的偏应用程序表达式,如 println _或 sum _,而且在代码的那个地方正 需要一 个函数
,你可以 去掉下 划线从 而表达 得更简 明。
someNumbers.foreach(println _)
你可以只是写成:
someNumbers.foreach(println)
最后一种格式仅在需要写函数的地方,如例子中的 foreach 调用,才能使用。
在不需要函数的情况下,尝试使用这种格式将引发一个编译错误。举例如下:
scala> val c = sum
: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 的偏应用函数语法凸显了 Scala 与经典函数式语言如 Haskell 或 ML
之间,设计折中的差异。在经典函数式语言中,偏应用函数被当作普通的例子。更进一步,这些语言拥有非常严格的静态类型系统能够暴露出你在偏应用中可能犯的所有错误。Scala
与指令式语言如 Java
关系近得多,在这些语言中没有应用所有参数的方法会被认为是错误的。进一步说,子类型推断的面向对象的传统和全局的根类型接受一些被经典函数式语言认为是错误的程序。
举例来说,如果你误以为 List 的 drop(n: Int)方法如 tail(),那么你会忘记你需要传递给drop
一个数字。你或许会写, “println(drop)” 果 Scala 采用偏应用函数在哪儿都 OK
的经典函数式传统,这个代码就将通过类型检查。然而,你会惊奇地发现这个 println 语句打印的输出将总是
!可能发生的事情是表达式 drop
将被看作是函数对象。因为 println 可以带任何类型对象,这个代码可以编译通过,但产生出乎意料的结果。
为了避免这样的情况,Scala 需要你指定显示省略的函数参数,尽管标志简单到仅用一个‘ _’。Scala
允许你仅在需要函数类型的地方才能省略这个仅用的 _。
闭包:
(x: Int) => x + more // more 是多少?
从这个函数的视点来看,more
是个自由变量:free variable,因为函数文本自身没有给出其含义。相对的, x变量是一个绑定变量:bound
variable,因为它在函数的上下文中有明确意义:被定义为函数的唯一参数,一个 Int 。
范围内没有任何 more 的定义,编译器会报错,只要有一个叫做 more 的什么东西同样的函数文本将工作正常。
依照这个函数文本在运行时创建的函数值(对象)被称为闭包:closure。 名称
源自于通过“捕获”自由变量的绑定对函数文本执行的“关闭”行动。
不带自由变量的函数文本,如 (x: Int) => x
+1,被称为封闭术语:closed term,这里术语:term
指的是一小部分源代码。因此依照这个函数文本在运行时创建的函数值严格意义上来讲就不是闭包,因为(x: Int)
=> x + 1 在编写的时候
就已经封闭了。但任何带有自由变量的函数文本,如(x: Int) => x +
more,都是开放术语:openterm。因此,任何依照(x: Int) => x + more
在运行期创建的函数值将必须捕获它的自由变量,more,的绑定。由于函数值是关闭这个开放术语(x: Int)
=> x + more 的行动的最终产物,得到的函数值将包含一个指向捕获的 more
变量的参考,因此被称为闭包。
依照(x: Int)=> x + more 创建的闭包看到了闭包之外做出的对more
的变化。反过来也同样。闭包对捕获变量
作出的改变在闭包之外也可见。
重复参数:
Scala 允许你指明函数的最后一个参数可以是重复的。这可以允许客户向函数传入可变长度参数列表。
想要标注一个重复参数,在参数的类型之后放一个星号。
scala> def echo(args: String*) =
for (arg
args) println(arg)
echo: (String*)Unit
这样定义,echo 可以被零个至多个 String 参数调用:
scala> echo()
scala> echo("one")
函数内部,重复参数的类型是声明参数类型的数组。
因此,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)
:7: error: type
mismatch;
found : Array[java.lang.String]
required: String
echo(arr)
要实现这个做法,你需要在数组参数后添加一个冒号和一个_*符号,像这样:
scala> echo(arr: _*)
尾递归:
1,def approximate(guess: Double): Double =
if (isGoodEnough(guess))
guess
else
approximate(improve(guess))
2,def approximateLoop(initialGuess: Double): Double = {
var guess =
initialGuess
while
(!isGoodEnough(guess))
guess =
improve(guess)
guess
}
就简洁性和避免 var 而言,第一个,函数式的胜出
尾调用优化
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 编译器可以应用一个重要的优化。注意递归调用是approximate
函数体执行的最后一件事。像approximate 这样,在它们最后一个动作调用自己的函数,被称为尾递归:tail
recursive。Scala 编译器检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的跳转。
def boom(x: Int): Int =
if (x == 0) throw new
Exception("boom!")
else boom(x - 1) +
1
这个函数不是尾递归,因为在递归调用之后执行了递增操作。
跟踪尾递归函数:
尾递归函数将不会为每个调用制造新的堆栈框架;所有的调用将在一个框架内执行。
尾递归的局限:
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 的调用的函数值。
尾调用优化受限于方法或嵌套函数在最后一个操作调用本身,而没有转到某个函数值或什么其它的中间函数的情况。