Scala函式與函數語言程式設計

weixin_34321977發表於2017-02-10

函式是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並不保證遍歷的順序, 若要求特定順序請使用reduceLeftreduceRight.

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專案上發揮函數語言程式設計的長處.

相關文章