函數的表現形式
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
。