说明:此博客是《Programming in Scala》的笔记,仅用于自己学习使用.
一、头等函数
1.函数字面量
Scala的函数是头等函数。你不仅可以定义和调用函数,还可以把它们写成匿名的字面量,并把它们作为值传递。
函数字面量被编译进类,并在运行期实例化为函数值。因此函数字面量和值的区别在于函数字面量存在于源代码,而函数值作为对象存在与运行期。这个区别很像类(源代码)和对象(运行期)之间的区别。
已下是对数执行递增操作的函数字面量的简单例子:
(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
如果你想让函数字面量包含多条语句,可以用花括号包住函数体,一行放一条语句,这样就组成了代码快。与方法一样,但函数值被调用时,所有语句将被执行,而函数的返回值就是最后一行表达式产生的值。
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方法。它以函数作为入参,并对每个元素调用该函数。下面是如何用它打印输出所有列表元素的代码:
scala> val someNumbers = List(1,2,3)
someNumbers:List[Int] = List(1,2,3)
scala> someNumbers.foreach((x:Int) => println(x))
1
2
3
另举一个例子,关于集合类型的filter方法。这个方法选择集合类型里可以通过用户提供的测试的元素。测试是通过调用作为入参的函数实现的。例如:
scala> someNumbers.filter((x:Int) => x > 1)
res6:List[Int] = List(2,3)
2.函数字面量的短路式
Scala提供了许多方法去除冗余信息并把函数字面量写的更简单。
一种让函数字面量更简介的方式是去除参数类型。因此,前面带过滤器的例子可以写成这样:
scala> someNumbers.filter((x) => x>1)
res7: List[Int] = List(2,3)
Scala编译器知道x一定是整数,因为它看到你立刻使用了这个函数过滤整数列表(由someNumbers暗示)。这被称为目标类型化。
某些参数的类型是被推断的,省略其外的括号,是第二种去除无用字符的方式。
scala> someNumbers.filter(x => x>1)
res7: List[Int] = List(2,3)
二、占位符语法
如果想让函数字面量更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数字面量内仅出现一次。比如,_>1对于检查值是否大于零的函数来说就是非常短的标注:
scala> sonmeNumbers.filter(_ > 1)
res9: List[Int] = List(2,3)
你可以把下划线看做表达式里需要被“填入”的空白。这个空白在每次函数被调用的时候用函数的参数填入。因此,函数字面量_ > 0与稍微冗长一点儿的x => x > 0 相同,演示如下:
scala> someNumbers.filter(x => x > 1)
res10: List[Int] = List(2,3)
有时你把下划线当做参数的占位符,编译器可能无法推断缺失的参数类型。例如:
scala val f = _ + _
<console>:4:error: missing parameter type for expanded
这种情况下,你可以使用冒号指定类型,如下:
scala> val f = (_:Int) + (_:Int)
f: (Int,Int) => Int = <function>
scala> f(5,10)
res11: Int = 15
请注意+将扩展成带两个参数的函数字面量。这样也解释了为何仅当每个参数在函数字面量中最多出现一次时,你才能使用这种短路格式。多个下划线指代多个参数,而不是单个参数的重复使用。第一个下划线代表第一个参数,第二个下划线代表第二个,第三个,…,如此类推。
三、部分应用函数
尽管前面的例子里下划线替代的只是单个参数,你还可以使用单个下划线替换整个参数列表。例如,写成print(),或者更好的方法你还可以写成println.下面是一个例子:
someNumbers.foreach(println _)
Scala把这种短路格式直接看做是你输入了下列代码:
someNumbers.foreach(x => println(x))
因此,这个例子中的下划线不是单个参数的占位符。它是整个参数列表的占位符。请记住要在函数名和下划线之间留一个空格。
以这种方式使用下划线时,就是正在写一个部分应用函数。Scala里,当你地啊用函数,传入任何需要的参数,实际是把函数应用到参数上。如给定下列函数:
scala> def sum(a:Int,b:Int,c:Int)=a+b+c
scala>sum(1,2,3)
res12: Int = 6
部分应用函数是一种表达式,你不需要提供函数需要的所有参数。代之以仅提供部分,或不提供所需参数。比如,要创建调用sum的部分应用表达式,而不提供任何3个所需参数,只要在”sum”之后放一个下划线即可。然后可以把得到的函数存入变量。举例来说:
scala> val a = sum _
a: (Int,Int,Int) => Int = <function>
有了这个代码,Scala编译器以部分应用函数表达式,sum _,实例化一个带3个缺失整数参数的函数值,并把这个新的函数值的索引赋值给变量a。当你把这个新函数值应用于3个参数之上时,他就地啊用sum,并传入这3个参数:
scala> a(1,2,3)
res13: Int = 6
现在,尽管sum 确实是一个偏函数,或许对你来说为什么这么称呼并不是很熟悉。这个名字源自于函数未被应用于它的所有参数。在sum 的例子里,它没有应用于任何参数。不过还可以通过提供某些但不是全部需要的参数表达一个偏函数。举例如下:
scala> val b = sum(1,_:Int,3)
b:(Int) => = <function>
这个例子里,你提供了第一个和最后一个参数给sum,但中间参数缺失。因此仅有一个参数缺失,Scala编译器会产生一个新的函数类。如下:
scala> b(2)
res15: Int = 6
在这个例子中其实是调用的sum(1,2,3)
四、闭包
到这里为止,所有函数字面量的例子仅参考传入的参数。例如,(x:Int)=>x>1里,函数体x>0用到的唯一变量x,被定义为函数参数。然而也可以参考定义在其它地方的变量:
(x: Int) => x + more //more是多少?
函数把”more”加入参考,但什么是more呢?从这个函数来看,more是个自由变量,因为函数字面量自身没有给出其含义。相对地,x变量是一个绑定变量,因为它在函数的上下文中有明确意义:被定义为函数的唯一参数是Int。如果你尝试独立使用这个函数字面量,范围内没有任何more的定义,编译器会报错说:
scala> (x : Int) => x + more
<console>:5:error: not found: value more
另一方面,只要有一个叫做more的某种东西,同样的函数字面量将正常工作:
scala> var more = 1
scala> val addMore = (x : Int) => x + more
scala> addMore(10)
res19: Int = 11
依照这个函数字面量在运行时创建的函数值(对象)被称为闭包。名称源自于通过“捕获”自由变量的绑定,从而对函数字面量执行的“关闭”行动。不带自由变量的函数字面量,如(x : Int) => x + 1,被称为封闭项,这里项指的是一小部分源代码。因此依照这个函数字面量在运行时创建的函数值严格意义上讲就不是闭包,因为(x : Int) => x + 1在编写的时候就已经封闭了。但任何带有自由变量的函数字面量,如(x : Int) => x + more,都是开放项。因此,任何一(x : Int) => x + more 为模板在运行期创建的函数值将必须捕获对自由变量more的绑定。因此得到的函数值将包含指向捕获的more变量的索引。又由于函数值是关闭这个开放项(x: Int) => x + more的行动的最终产物,因此被称为闭包。
这个例子带来一个问题:如果more在闭包创建之后被改变了会发生什么事?Scala里,答案是变化如下:
scala> more = 9999
scala> addMore(10)
res21: Int = 10009
直觉上,Scala的闭包捕获了变量本身,而不是变量指向的值。就像前面演示的例子,依照(X: Int) => x + more创建的闭包看到了闭包之外做出的对more的变化。反过来也同样。闭包对捕获变量做出的改变在闭包之外也可见。下面是一个例子:
scala> val someNumbers = List(-11,-10,-5,0,5,10)
scala> var sum = 0
scala> someNumbers.foreach(sum += _) //sum是自由变量
scala> sum
res23: Int = -11
例子用一个循环的方式计算List的累加和。变量sum处于函数字面量sum += _的外围,函数字面量把数累加到sum上。尽管这是一个在运行期间改变sum的闭包,作为结果的累加值,-11,仍然在闭包之外可见。
五、重复参数
Scala中,你可以指明函数的最后一个参数是重复的。从而允许客户向函数传入可变长度参数列表。想要标注一个重复参数,可以在参数的类型之后放一个星号。例如:
scala> def echo(args: String*) =
for(arg <- args) println(arg)
scala> echo()
scala> echo("one)
one
scala> echo("hello","world")
hello
world
函数内部,重复参数的类型是声明类型的数组。因此,echo函数里被声明为类型”String”的args的类型实际上是Array[String]。然而,如果你一个合适类型的数组,并尝试把它当做重复参数传入,你会得到一个编译器错误。
要实现这个做法,你需要在数组参数后面添加一个冒号和一个_*符号,像这样:
scala> val arr = Array("How","are","you?")
scala> echo(arr: _*)
How
are
you?