函数与闭包详解

函数的表现形式

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

但是我们可以直接调用Listexists方法: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

柯里化的代码: 
这里是发生了两次函数调用,第一个函数调用了带单个的名为xInt参数,并返回第二个函数的函数值。第二个函数带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

posted on 2018-08-29 15:18  duanxz  阅读(4327)  评论(0编辑  收藏  举报