程式扎記: [Scala 小學堂] Scala Gossic : 了解更多 - 定義函式 (一級函式)

標籤

2016年5月17日 星期二

[Scala 小學堂] Scala Gossic : 了解更多 - 定義函式 (一級函式)

轉載自 這裡
前言 :
Scala 是一個可直譯、編譯、靜態、運行於 JVM 之上、可與 Java 互操作、融合物件導向編程特性與函式編程風格的程式語言. Scala 本身具有擴充性,不過這必須了解更多語法特性與細節.

一級函數 :
在 Scala 中,函式是一級(First-class)公民,也就是說,在Scala中,函式是物件。如果你要定義一個函式,基本上是使用 def 來定義,如同 簡單的函式 中所說明過的,例如你要定義一個最大值的函式 :
  1. def max(m: Int, n: Int) = if(m > n) m else n  
你可以用函式常量(Function literal的方式來定義一個函式,執行時期將會為其產生函式值Function value)。例如,上面的 max 函式,可以用以下的方式定義 :
  1. val max = (m: Int, n: Int) => if(m > n) m else n  
你使用 => 定義函式常量,在上例中,=> 左邊的 (m: Int, n: Int) 定義了函式的參數與類型,=> 右邊則是函式本體,max 的型態呢?實際上是 (Int, Int) => Int,也就是實際上完整的宣告應該是 :
val max: (Int, Int) => Int = (m: Int, n: Int) => if(m > n) m else n


這說明了宣告函式物件的參考名稱時,如何指定型態型態,這表示你可以將一個函式物件進行傳遞,例如,在 選擇 排序 的實作中,你可以傳入一個函式物件來改變排序是要由小而大或由大而小 :
- FirstFunc1.scala 代碼 :
  1. def selection(number: Array[Int], order: (Int, Int) => Boolean) {  
  2.         def mm(m: Int, j: Int):Int= {  
  3.                 if(j==number.length) m  
  4.                 else if(order(number(j), number(m))) mm(j, j+1)  
  5.                 else mm(m, j+1)  
  6.         }  
  7.         for(i <- span="">0 until number.length-1) {  
  8.                 val m = mm(i, i+1)  
  9.                 if(i!=m)  
  10.                 swap(number, i, m)  
  11.         }  
  12. }  
  13.   
  14. def swap(number: Array[Int], i:Int, j: Int) {  
  15.         val t = number(i)  
  16.         number(i) = number(j)  
  17.         number(j) = t  
  18. }  

如果你想要排序由小而大,則可以這麼使用函式 :
  1. val arr1 = Array(25178)  
  2. selection(arr1, (a: Int, b: Int) => a < b)  
  3. println(arr1.mkString(","))  
如果你想要排序由大而小,則可以這麼使用函式 :
  1. val arr2 = Array(25178)  
  2. selection(arr2, (a: Int, b: Int) => a > b)  
  3. println(arr2.mkString(","))  
你可以利用 Scala 的類型推斷來簡化函式字面量的撰寫方式,例如在上例中,可以從 selection 函式的參數宣告上,得知所傳入函式值的兩個參數型態,所以可以省略函式字面量撰寫時的參數型態。例如 :
  1. val arr2 = Array(25178)  
  2. selection(arr2, (a, b) => a > b)  
  3. println(arr2.mkString(","))  
如果函式字面在撰寫時,=> 右邊的演算在使用參數時,有與參數相同的順序,則可以使用佔位字元語法(Placeholder syntax),省略參數列的宣告與 v 的使用,例如 :
  1. val arr2 = Array(25178)  
  2. selection(arr2, (_: Int) > (_: Int))  
  3. println(arr2.mkString(","))  
上例中,第一個 _ 代表傳入的第一個引數,第二個代表傳入的第二個引數,型態都是 Int。如果可以利用 Scala 的類型推斷,則可以再簡化上例,例如 :
  1. val arr2 = Array(25178)  
  2. selection(arr2, _ > _)  
  3. println(arr2.mkString(","))  
使用佔位字元語法的方式,若要省略型態部份,必須在可以推斷類型的情況,例如 :
  1. val f = (_: Int) + (_: Int)    // 這樣 OK  
  2. println(f(12))               // 顯示 3   
但這樣就不行,因為 Scala 無法推斷出參數的類型為何 :
val f = _ + _ // 錯誤 missing parameter type for expanded function


在 Scala 中,經常可以看到傳遞函式的寫法,例如群集物件的 foreach 方法,可以接受一個函式物件,當中定義如何處理群集中每個元素 :
Array("a", "b", "c").foreach(x => print(x.toUpperCase)) // 顯示 ABC


要探討實際上類型推斷可以達到什麼程度會蠻複雜的,基本上建議的簡化撰寫原則是,在可以使用類型推斷的時候使用類型推斷,在無法使用類型推斷時,再標示出型態資訊。如果在簡單的函式定義中,參數的使用順序與參數列宣告的順序相同時,使用佔位字元寫法 :
Array(4, 8, 1, 6, 3, 7, 9).filter(_ > 5).foreach(print(_)) // 顯示 8、6、7、9


如果你的函式字面量演算內容比較繁多,則可以使用 {},例如 :
  1. val max = (m: Int, n: Int) => {  
  2.               if(m > n)   
  3.                  m   
  4.               else n  
  5.           }  
在 Scala 中,函式常量的寫法,其實會由編譯器自動產生出類別,並根據該類別建立函式物件,這個由編譯器動態產生的類別,有個 apply 方法,正如Scala語法的一致性,如果你想呼叫 apply 方法,其實可以直接使用 (),這可以由以下的範例來證明 :
  1. val max = (m: Int, n: Int) => if(m > n) m else n  
  2. println(max(1020))          // 顯示 20  
  3. println(max.apply(1020))    // 顯示 20  
所以實際上,使用 def 定義函式,與使用函式常量的寫法來產生函式物件是不同的,如果你要以 def 定義的函式來產生函式物件,則可以使用 部份套用函式(Partially applied function 的語法.
在支援函式為一級物件的語言中,對於程式的撰寫可以有更多的彈性,例如,在 多維矩陣轉一維矩陣 中,你可能原先設計了兩個函式 :
- FirstFunc2.scala 代碼 :
  1. def toOneByRow(array: Array[Array[Int]]) = {  
  2.     val arr = new Array[Int](array.length * array(0).length)  
  3.     for(row <- nbsp="" span="">0 until array.length; column <- nbsp="" span="">0 until array(0).length) {  
  4.         arr(row * array(0).length + column) = array(row)(column) // 差異部分  
  5.      }  
  6.     arr  
  7. }  
  8.       
  9. def toOneByColumn(array: Array[Array[Int]]) = {  
  10.     val arr = new Array[Int](array.length * array(0).length)  
  11.     for(row <- nbsp="" span="">0 until array.length; column <- nbsp="" span="">0 until array(0).length) {  
  12.         arr(row + column * array.length) = array(row)(column) // 差異部分  
  13.      }  
  14.     arr  
  15. }  

仔細觀察,你會發現,除了上面範例代碼標示差異不同之外(也就是計算索引的部份),演算法的其它部份是相同的,演算實作時,這樣的重複結構並不鼓勵,如果將來你改變演算 法,則要修改一個函式時,複製至另一個函式,然後修改不同的部份計算索引的部份,會造成維護上的麻煩。如果你可以傳遞函式物件,則可以改寫為以下的方式 :
- FirstFunc3.scala 代碼 :
  1. def toOneByRow(array: Array[Array[Int]]) = {  
  2.     toOne(array, _ * array(0).length + _)  
  3. }  
  4.       
  5. def toOneByColumn(array: Array[Array[Int]]) = {  
  6.     toOne(array, _ + _ * array.length)  
  7. }  
  8.       
  9. def toOne(array: Array[Array[Int]], index: (Int, Int) => Int) = {  
  10.     val arr = new Array[Int](array.length * array(0).length)  
  11.     for(row <- nbsp="" span="">0 until array.length; column <- nbsp="" span="">0 until array(0).length) {  
  12.         arr(index(row, column)) = array(row)(column)  
  13.     }  
  14.     arr      
  15. }  


沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!