函数与闭包详解
函数的表现形式
1、方法
定义:定义函数最通用的方法就是作为某个对象的成员。这种函数被称为方法。
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)
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
2、本地函数
上面的processFile
方法展示了函数式编程风格的重要设计原则:程序应该被分割理解成若干个小的函数,每一块都实现一个完备的任务,每一块都很小。这利于让我们去组合更为复杂的事物。但是,这种风格有一个问题,所有这些帮助函数(即每个小块)的名称可能会污染程序的命名空间。Java中的private
在Scala中一样有效,但Scala还提供了另一种方式:你可以把函数定义在别的函数之内,就像本地变量那样,这种本地变量只在它的代码块中可见。
def processFile(fileName: String,width: Int){
def processLine(filename:String,width:Int,line:String){
if(line.length>width){
println(filename+":"+line.trim)
}
}
val source = Source.fromFile(fileName)
for(line<-source.getLines) processLine(filename,width,line)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
本地函数可以直接访问包含其函数的参数:
def processFile(fileName: String,width: Int){
def processLine(line:String){
if(line.length>width){
println(filename+":"+line.trim)
}
}
val source = Source.fromFile(fileName)
for(line<-source.getLines) processLine(filename,width,line)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
3、头等函数
Scala的函数是头等函数。我们不仅可以定义和调用函数,还可以把他们写成匿名的字面量并作为值传递。函数字面量被编译进类,并在运行期实例化为函数。因此函数字面量和值的区别在于函数字面量存在于编译期,值出现于运行期。
函数字面量的一些例子:
(x:Int) => x+1
- 1
var increase = (x:Int) => x+1
increase = (x:Int) => x+999
increase = (x:Int) => {
println("wang")
println("zha")
println("bangbangda")
x+1
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
所有的集合类型都可以用foreach
方法来遍历,foreach
方法以函数作为入参,并对每个元素调用该函数:
var someNumbers = List(1,5,2,88,3)
someNumbers.foreach((x:Int)=>println(x))
//函数字面量的短格式
someNumbers.foreach(x=>println(x))
- 1
- 2
- 3
- 4
- 5
占位符语法和部分应用函数
占位符语法可以替代部分参数。
someNumbers.foreach(_=>println)
val f = (_:Int)+(_:Int)
- 1
- 2
- 3
- 4
部分应用函数可以替代整个参数列表。
def sum(a:Int,b:Int,c:Int) = a+b+c
val a = sum _
a(1,2,3)
- 1
- 2
- 3
上面这个代码的流程是:名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照部分应用函数表达式sum _
,自动产生的一个实例。注意下划线前面要有一个空格,防止把sum_当成一个方法。
sum(1, _:Int,6)
- 1
我们再看看上述的一行代码,这是另一种用途,在例子中,提供了第一个和第三个参数,中间的参数缺失。因为这个参数缺失,编译器会产生一个新的函数类,其apply
方法带一个参数。在使用一个参数调用时,这个新产生的函数的apply
方法调用sum
,传入1、6
,传递给函数的参数。
如果你正在写一个省略所有参数的偏程序表达式(即部分应用函数表达式),如println _
或sum _
,而且在代码的那个地方正需要一个函数,你可以去掉下划线从而更加简明地表达。
someNumbers.foreach(println _)
someNumbers.foreach(println)
- 1
- 2
注意只有在需要写函数的地方才可以省略下划线。比如foreach
入参是函数,所以println _
可以省略成println
。而val a = sum _
却不能写成val a = sum
。
闭包
任何以函数字面量为模版创建的函数对象为闭包,前提,该函数字面量中包含自由变量,即闭包的产生过程中,闭包需要动态绑定这个自由变量。
val addMore = (x:Int)=>x+more
addMore:闭包
more:自由变量
- 1
- 2
- 3
- 4
资料里叙述了很多,实质上说的就是,自由变量和当次传入的值进行动态绑定。看下面代码:
def makeIncr(more:Int) = (x:Int)=>x+more
val incr1 = makeIncr(1)
val incr2 = makeIncr(9999)
incr1(10) // 11
incr2(10) //10009
- 1
- 2
- 3
- 4
- 5
重复参数
Scala中,我们可以指定函数的最后一个参数是重复的。满足我们传入可变长度参数列表。想要标注一个重复参数,可在参数类型后面放一个星号:
def echo(args:String*) = foreach(println)
- 1
重复参数的类型声明实质上是一个数组。因此,上述echo
函数里被声明的其实是一个Array[String]
。我们也可以通过下面的这种方式传入数组:
def echo(arr: _*) = foreach(println)
val arr = Array("what's","up",",man")
echo(arr)
- 1
- 2
- 3
_*
表示把arr
的每个元素当成参数传入,而不是单一的元素传给echo
。
控制抽象
1、减少代码重复
所有的函数都可以被划分为通用部分和非通用部分。通用部分是函数体,非通用部分是入参。当我们把函数值作为参数时,非通用部分就代表着不同的算法。在这种函数每一次调用中,我们都可以把不同的函数作为入参传入,被调用的函数每次选用参数的时候调用传入的参数值。这种高阶函数让我们有机会去简化代码。
下面我们来看一个例子,以加深理解函数值(函数字面量)用来简化代码:
常规代码:
object FileMatcher{
private def filesHere = (new java.io.File(".")).listFiles
def fileEncoding(query:String) = {
for(file<-filesHere; if (file.getName.endsWith(query))) yield file
}
def fileEncoding(query:String) = {
for(file<-filesHere; if (file.getName.contain(query))) yield file
}
def fileEncoding(query:String) = {
for(file<-filesHere; if (file.getName.matches(query))) yield file
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
简化代码第一步(利用函数字面量):
object FileMatcher{
private def filesHere = (new java.io.File(".")).listFiles
def filesMatching(query:String,matcher:(String,String)=>Boolean) = {
for(file<-filesHere;if(matcher(file.getName,query))) yield file
}
def filesEnding(query:String) = filesMatching(query, _.endsWith(_))
def filesContaining(query:String) = filesMatching(query, _.contain(_))
def filesRegex(query:String) = filesMatching(query, _.matches(_))
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
简化第二步(利用闭包):
object FileMatcher{
private def filesHere = (new java.io.File(".")).listFiles
def filesMatching(matcher:String=>Boolean) = {
for(file<-filesHere;if(matcher(file.getName))) yield file
}
def filesEnding(query:String) = filesMatching( _.endsWith(query))
def filesContaining(query:String) = filesMatching( _.contain(query))
def filesRegex(query:String) = filesMatching( _.matches(query))
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2、简化客户代码
举个例子:我们判断一个传入的值是否被包含在集合中
通常实现代码:
def contain(nums:List[String]):Boolean={
var exists =false
for(n<-nums;if(n<0)) exists =true
}
- 1
- 2
- 3
- 4
但是我们可以直接调用List
的exists
方法:nums.exists(_<0)
。exists方法代表了控制抽象。再举一个例子,如果让我们再写一个是否集合中是否含有奇数或者偶数,我们一定也会选择函数值为入参的高阶函数:
def containsOdd(nums:List[Int]) = nums.exists(_%2 == 0)
def containsNeg(nums:List[Int]) = nums.exists(_%2 != 0)
def exists(compare:Int=>Boolean){
for(n<-nums;if(compare(n))) true
}
- 1
- 2
- 3
- 4
- 5
- 6
3、柯里化的函数式编程技巧
未被柯里化的代码:
这里是一个完整的拥有2个入参的函数。
def plainOldSum(x:Int,y:Int) = x+y
plainOldSum(1,3) //4
- 1
- 2
柯里化的代码:
这里是发生了两次函数调用,第一个函数调用了带单个的名为x
的Int
参数,并返回第二个函数的函数值。第二个函数带Int
参数y
。调用过程等价于def first(x:Int) = (y:Int) => x+y
def plainOldSum(x:Int)(y:Int) = x+y
plainOldSum(1)(3) //4
- 1
- 2
简单地说第一步,plainOldSum(1)
返回了一个匿名函数,(y:Int) = 1+y
,第二步,plainOldSum(3)
,最终结果为4
。