函數是scala的重要組成部分, 本文將探討scala中函數的應用.
scala作為支持函數式編程的語言, scala可以將函數作為對象即所謂"函數是一等公民".
函數定義
scala源文件中可以定義兩類函數:
-
類方法: 類聲明時定義, 由類實例進行調用
-
局部函數: 在函數內部定義, 作用域只限於定義它的函數內部
這里只關注函數定義相關內容, 關於類的有關內容請參考面向對象的相關內容.
scala使用def
關鍵字定義函數:
def test() {
println("Hello World!");
}
因為是靜態類型語言, 定義含參數和返回值的函數需要指定類型, 語法略有不同:
def add(x:Int, y:Int): Int = {
return x + y;
}
scala支持默認參數:
def add(x:Int = 0, y:Int = 0):Int = {
return x + y;
}
可以指定最后一個參數為可變參數, 從而接受數目不定的同類型實參:
scala> def echo (args: String *) { for (arg <- args) println(arg) }
scala> echo("Hello", "World")
Hello
World
String *
類型的參數args實際上是一個Array[String]
實例, 但是不能將一個Array作為參數傳給args.
若需傳遞Array作為實參,需要使用arr :_*
傳遞實參:
scala> val arr= Array("Hello" , "World")
arr: Array[String] = Array(Hello, World)
scala> echo(arr: _*)
Hello
World
命名參數允許以任意順序傳入參數:
scala> def speed(dist:Double, time:Double):Double = {return dist / time}
scala> speed(time=2.0, dist=12.2)
res28: Double = 6.1
參數傳遞
scala的參數傳遞采用傳值的方式, 參數被當做常量val而非變量var傳入.
當我們試圖編寫一個swap函數時,出現錯誤:
scala> def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
<console>: error: reassignment to val
def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
^
<console>: error: reassignment to val
def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
^
scala中的標識符實際是引用而非對象本身, 這一點與Java相同。 類實例中的屬性和容器的元素實際上只保存了引用, 並非將成員自身保存在容器中。
不熟悉Java的同學可以將對象和引用類比為C中的變量和指針
val將一個對象設為常量, 使得我們無法修改其中保存的引用,但是允許我們修改其引用的其它對象.
以二維數組val arr = Array(1,2,3)
為例。 因為arr為常量,我們無法修改arr
使其為其它值, 但我們可以修改arr引用的對象arr(0)
使其為其它值:
scala> val arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3)
scala> arr = Array(2,3,4)
<console>:12: error: reassignment to val
arr = Array(2,3,4)
^
scala> arr(0) = 2
arr: Array[Int] = Array(2, 2, 3)
參數傳遞過程同樣滿足這個性質:
scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3)
scala> def fun(arr:Array[Int]):Array[Int] = {arr(0) += 1; return arr;}
fun: (arr: Array[Int])Array[Int]
scala> fun(arr)
res: Array[Int] = Array(3, 2, 3)
scala> arr
arr: Array[Int] = Array(3, 2, 3)
換名傳遞
上述參數傳遞采用傳值的方式傳遞: 在函數調用時實參值被傳入函數執行過程中參數值不會因為實參值改變而發生改變。
換名傳遞則不立即進行參數傳遞, 只有參數被訪問時才會去取實參值, 即形參成為了實參的別名.
換名傳遞可以用於實現惰性取值的效果.
換名傳遞參數用: =>
代替:
聲明, 注意空格不能省略.
def work():Int = {
println("generating data");
return (System.nanoTime % 1000).toInt
}
def delay(t: => Int) {
println(t);
println(t);
}
scala> delay(work())
generating data
247
generating data
143
從結果中可以注意到work()
函數被調用了兩次, 並且換名參數t的值發生了改變.
換名參數只是傳遞時機不同,仍然采用val的方式進行傳遞.
函數字面量
函數字面量又稱為lambda表達式, 使用=>
符號定義:
scala> var fun = (x:Int) => x + 1
fun: Int => Int = $$Lambda$1422/1621418276@3815c525
函數字面量是一個對象, 可以作為參數和返回值進行傳遞.
使用_
逐一替換普通函數中的參數 可以得到函數對應的字面量:
scala> def add(x:Int, y:Int):Int = {return x + y}
add: (x: Int, y: Int)Int
scala> var fun = add(_,_)
fun: (Int, Int) => Int = $$Lambda$1423/1561881364@37b117dd
部分應用函數與偏函數
使用_
代替函數參數的過程中,如果只替換部分參數的話則會得到一個新函數, 稱為部分應用函數(Partial Applied Function):
scala> val increase = add(_:Int, 1)
increase: Int => Int = $$Lambda$1453/981330853@78fc5eb
偏函數是一個數學概念, 是指對定義域中部分值沒有定義返回值的函數:
def pos = (x:Int) => x match {
case x if x > 0 => 1
}
高階函數
函數字面量可以作為參數或返回值, 接受函數字面量作為參數的函數稱為高階函數.
scala內置一些高階函數, 用於定義集合操作:
collection.map(func)
將集合中每一個元素傳入func並將返回值組成一個新的集合作為map函數的返回值:
scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3)
scala> arr.map(x=>x+1)
res: Array[Int] = Array(2, 3, 4)
上述示例將arr中每個元素執行了x=>x+1
操作, 結果組成了一個新的集合返回.
collection.flatMap(func)
類似於map, 只不過func返回一個集合, 它們的並集作為flatMap的返回值:
scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3)
scala> arr.flatMap(x=>Array(x,-x))
res: Array[Int] = Array(1, -1, 2, -2, 3, -3)
上述示例將arr中每個元素執行x=>Array(x, -x)
得到元素本身和它相反數組成的數組,最終得到所有元素及其相反數組成的數組.
collection.reduce(func)
中的func接受兩個參數, 首先將集合中的兩個參數傳入func,得到的返回值作為一個參數和另一個元素再次傳入func, 直到處理完整個集合.
scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3)
scala> arr.reduce((x,y)=>x+y)
res: Int = 6
上述示例使用reduce實現了集合求值. 實際上, reduce並不保證遍歷的順序, 若要求特定順序請使用reduceLeft
或reduceRight
.
zip函數雖然不是高階函數,但是常和上述函數配合使用, 這里順帶一提:
scala> var arr1 = Array(1,2,3)
arr1: Array[Int] = Array(1, 2, 3)
scala> var arr2 = Array('a', 'b', 'c')
arr2: Array[Char] = Array(a, b, c)
scala> arr1.zip(arr2)
res: Array[(Int, Char)] = Array((1,a), (2,b), (3,c))
高階函數實際上是自定義了控制結構:
scala> def twice(func: Int=>Int, x: Int):Int = func(func(x))
twice: (func: Int => Int, x: Int)Int
scala> twice(x=>x*x, 2)
res: Int = 16
twice
函數定義了將函數調用兩次的控制結構, 因此實參2被應用了兩次x=>x*x
得到16.
柯里化
函數的柯里化(currying)是指將一個接受n個參數的函數變成n個接受一個參數的函數.
以接受兩個參數的函數為例,第一個函數接受一個參數 並返回一個接受一個參數的函數.
原函數:
scala> def add(x:Int, y:Int):Int = {return x+y}
add: (x: Int, y: Int)Int
進行柯里化:
scala> def add(x:Int)= (y:Int)=>x*y
add: (x: Int)Int => Int
這里沒有指明返回值類型, 交由scala的類型推斷來決定. 調用柯里化函數:
scala> add(2)(3)
res10: Int = 6
scala> add(2)
res11: Int => Int = $$Lambda$1343/1711349692@51a65f56
可以注意到add(2)
返回的仍是函數.
scala提供了柯里化函數的簡化寫法:
scala> def add(x:Int)(y:Int)={x+y}
add: (x: Int)(y: Int)Int
本文介紹了一些關於scala函數式編程(functional programming, FP)的特性, 在這里簡單介紹一下函數式編程范式.
函數式編程中, 函數是從參數到返回值的映射而非帶有返回值的子程序; 變量(常量)也只是一個量的別名而非內存中的存儲單元.
也就是說函數式編程關心從輸入到輸出的映射, 不關心具體執行過程. 比如使用map對集合中的每個元素進行操作, 可以使用for循環進行迭代, 也可以將元素分發到多個worker進程中處理.
函數式編程可理解為將函數(映射)組合為大的函數, 最終整個程序即為一個函數(映射). 只要將數據輸入程序, 程序就會將其映射為結果.
這種設計理念需要滿足兩個特性. 一是高階函數, 它允許函數進行復合; 另一個是函數的引用透明性, 它使得結果不依賴於具體執行步驟只依賴於映射關系.
結果只依賴輸入不依賴上下文的特性稱為引用透明性; 函數對外部變量的修改被稱為副作用.只通過參數和返回值與外界交互的函數稱為純函數,純函數擁有引用透明性和無副作用性.
不可變對象並非必須, 但使用不可變對象可以強制函數不修改上下文. 從而避免包括線程安全在內很多問題.
函數式編程的特性使得它擁有很多優勢:
-
函數結果只依賴輸入不依賴於上下文, 使得每個函數都是一個高度獨立的單元, 便於進行單元測試和除錯.
-
函數結果不依賴於上下文也不修改上下文, 從而在並發編程中不需要考慮線程安全問題, 也就避免了線程安全問題帶來的風險和開銷. 這一特性使得函數式程序很容易部署於並行計算和分布式計算平台上.
函數式編程在很多技術社區都是有着廣泛爭議的話題, 筆者認為"什么是函數編程","函數式編程的精髓是什么"這類問題並不重要。
作為程序員應該考慮的是"函數式編程適合解決什么問題?它有何有缺?"以及"何時適合應用函數式編程?這個問題中如何應用函數式編程?".
函數式編程並非"函數式語言"的專利. 目前包括Java,Python在內的, 越來越多的語言開始支持函數式特性, 我們同樣可以在Java或Python項目上發揮函數式編程的長處.