函數與閉包詳解


函數的表現形式

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM