Scala Tutorial(3)變數與函式
Scala Tutorial(2)準備開發環境、Scala 中的四則運算 << 前情 上次我們看到了在 Scala 當中,所有的東西都是物件,而所謂的運算元只是呼叫類別中的方法而已。 有了這些基礎,我們接下來要來看在 Scala 中要如何宣告我們自己的變數以及定義函式,以及利用我們已經熟悉的物件導向概念,來看 Functional Programming 中所謂的 High Order Function 到底是什麼東西。 Scala 中的變數宣告由於 Scala 是靜態型別的程式語言,因此就如同 Java 一樣,在 Scala 所有的變數在使用前都需要經過宣告,但比較特別的一點是由於 Scala 具有型別推論的功能,因此大多數的時候我們不需要宣告變數的型態。 此外,Scala 中的變數有兩種-- 在下面的例子中我們分別宣告了兩個變數,其中一個使用 scala> val readOnly = "This is read only" readOnly: String = This is read only scala> var changeable = "This could be changed" changeable: String = This could be changed scala> readOnly = "This is new string" <console>:8: error: reassignment to val readOnly = "This is new string" ^ scala> changeable = "This is new string" changeable: String = This is new string 另一件值得注意的事情,是雖然我們沒有需告 換句話說,如果我們試著將整數指定給 scala> var changeable = "This could be changed" changeable: String = This could be changed scala> changeable = 123 <console>:8: error: type mismatch; found : Int(123) required: String changeable = 123 ^ 雖然說大多數的時候 Scala 都能自動從等號後的運算式中推測出正確的型別,但有的時候他推測的型別並不一定是你要的,這個時候我們可以明確地告知 Scala 編譯器該變數的型別。 和 Java 中不同,在 Scala 中型別資訊是放在變數之後,並且以 val x: Int = 30 + 20 // 和 Scala 編譯器推論出的型別相同 val y: Long = 30 // Scala 編譯器推論的是 Int,但指定到相容型別的 Long 變數 val z: Any = new String("Hello World") // Scala 編譯器推論的是 String 型別,但可以放到父型別的 Any // 編譯錯誤,String 和 Long 不相容 // val a: Long = new String("Hello World") 上述的例子中,變數 這裡值得注意的是 不過這邊要特別注意的是,在 Scala 中的 舉例而言,我們知道在 Java 中 StringBuilder 物件被建立之後,其狀態是可以被改變的,所以即便我們使用 val builder = new StringBuilder println("Current String:" + builder.toString) builder.append("I'm not empty") println("Current String:" + builder.toString) // 物件狀態被改變了 // 編譯錯誤,builder 不能再被指到其他的物件 // builder = new StringBuilder 看到這裡讀者可能會覺得好奇,為什麼 Scala 中的變數宣告會分成 這是由於 Scala 是融合了物件導向以及 Functional Programming 這兩個編程典範的程式語言,而在這兩個編程典範當中對於「變數」這個東西有不同的概念。 在我們熟悉的物件導向中,變數多數的時候代表該物件的狀態,而隨著程式的流程與時間的經過,物件的狀態會跟著改變,而物件中的變數也會被重新指定。 但是在嚴格的純 Functional Programming 環境中(例如著名的 Haskell 語言),「變數」的概念和數學中的代數比較接近,當變數一經指定過後其值就是固定的,變數只是讓算式更清楚易懂,而不影響到實際運算的結果。 在 Scala 中則是兩種方式都有提供,但鼓勵使用者優先使用 但另一方面,如果使用者有合理的理由需要讓變數可以重新被賦值,Scala 也提供了 在往後篇章當中,我們會看到 Scala 裡更多這種「提供各種不同的工具,鼓勵使用某一種特定的方式,但當你真的需要的時候也不會擋路」的設計思維。 Scala 中的函式宣告註:從這節開始,我們將使用 script 模式來撰寫範例檔案,以方便讀者修改與執行。 了解變數的宣告之後,接下來我們就可以進入函式宣告的部份了。 Scala 中的函式宣告和 Java 的語法有相當大的差異,主要有以下幾點要注意:
雖然這些規則看起來好像相當複雜,但實際上使用後會發現是相當值覺的,所以在這一節當中,我們會以舉例為主。 從最簡單的,一個不接受任何參數,也沒有返回值的函式開始: def helloWorld() { println("Hello World") } helloWorld() // 呼叫函式的方法和 Java 相同 你可能會注意到這個函式沒有任何的型別資訊,這是因為若函式的標頭與函式的內容若沒有等號連接的話,則 Scala 會視其返回值的型態為 Unit,即沒有返回值。(Scala 官方已不建議使用這樣的語法,詳見[註一]) 上面的函式與下面兩個實際上是相同的: // 讓 Scala 自動使用最後一個 expression 的型態做為函式的返回型別,由於 println 的返回值 // 型別是 Unit,所以 helloWorld 的返回值型別也會是 Unit def helloWorld() = { println("Hello World") } // 明確宣告 helloWorld2 這個函式的返回值型別是 Unit def helloWorld2(): Unit = { println("Hello World") } 接下來,我們可以將 “Hello World" 字串抽出變成這個函式的參數: // 注意在 Scala 中函式參數的型別是不能省略的 def helloWorld(prompt: String) = { println(prompt) } helloWorld("Hello World") helloWorld("Hello World II") // 編譯錯誤:prompt 沒有型別資訊 /* * def helloWorld(prompt) = { * println(prompt) * } */ 除了一般的參數定義和呼叫外,Scala 也提供了參數預設值,以及在呼叫函式時使用 named argument 的功能,所以我們可以替參數直接指定預設值,並在在呼叫函式時是以名稱而非位置的方式指定參數的值。 def printOneLine(line: String = "I'm line 1") = { println(line) } def printTwoLine(line1: String, line2: String) = { println(line1) println(line2) } printOneLine("Hello World") // 印出 Hello World printOneLine() // 印出 "I'm line 1" // 印出: // Line1 // Line2 printTwoLine("Line1", "Line2") // 印出: // SecondArgument // FirstArgument printTwoLine(line2 = "FirstArgument", line1 = "SecondArgument") 接著我們來看看有返回值的函式要怎麼宣告吧!首先是最簡單的方式:僅宣告參數型別,函式返回值的型別則由 Scala 編譯器推論: def add(x: Int, y: Int) = x + y val sum = add(30, 40) println("sum = " + sum) // 以下這幾種宣告方式都是合法的: // def add(x: Int, y: Int) = { x + y} // def add(x: Int, y: Int) = x + y // def add(x: Int, y: Int): Int= { x + y} // def add(x: Int, y: Int): Int = x + y 在這個例子中,函式 此外,若我們不確定 Scala 推論出的函式返回型別是什麼,可以透過 Scala REPL 的幫助來檢視,舉例而言,若我們不確定一個讓 Double 與 Int 相加的函式的返回型別,那麼可以直接在 REPL 中定義這個函式: scala> def add(x: Int, y: Double) = x + y add: (x: Int, y: Double)Double 這樣子 Scala REPL 就會告訴我們, 接下來我們來看一下若函式的內容中有 // test.scala def max(a: Int, b: Int) = { if (a > b) { return a } else { return b } } println(max(5, 10)) 若執行上述的程式的話,Scala 會發出抱怨,告訴我們因為 max 這個函式中有 brianhsu@VaioPro13 ~ $ scala test.scala /home/brianhsu/test.scala:4: error: method max has return statement; needs result type return a ^ /home/brianhsu/test.scala:6: error: method max has return statement; needs result type return b ^ two errors found 如果我們要讓上面這段程式正確執行的話,那麼就要明確告知 Scala 編譯器這個函式的返回值: // test.scala def max(a: Int, b: Int): Int = { if (a > b) { return a } else { return b } } println(max(5, 10)) 但即便如此,這個程式碼仍然不是 Scala 風格的程式碼,在 Scala 的程式中我們相當少用到 return 關鍵字,這是因為 Scala 中大部份的敘述都是有返回值的,而大部份的情況下,最終都可以把 return 的邏輯簡化成函式的最後一個敘述句。 在上面的例子中,由於 Scala 的 if / else 敘述句是有返回值的,其返回值就是被選取到的分支的最後一個敘述句: scala> val resultOfTrueBranch = if (true) { | println("First expression inside true branch") | 1 | } else { | println("First expression inside false branch") | 2 | } First expression inside true branch resultOfTrueBranch: Int = 1 scala> val resultOfFalseBranch = if (false) { | println("First expression inside true branch") | 1 | } else { | println("First expression inside false branch") | 2 | } First expression inside false branch resultOfFalseBranch: Int = 2 由於 Scala 的 if / else 敘述句具有這樣的特性,所以我們的 def max(a: Int, b: Int) = if (a > b) { a } else { b } // 也可以明確標示返回值型別 // def max(a: Int, b: Int): Int = if (a > b) { a } else { b } println(max(5, 10)) // 出出 10 另外一個要注意的,就是如果該函式是遞迴函式,則其返回值也是要被名確地宣告的,否則 Scala 會發出編譯錯誤。舉例來說,若我們撰寫了一個用遞回方式加總 1 到 n 的函式,那麼該函式的返回值型別一定要被明確地宣告出來: // 編譯錯誤:recursive method sum needs result type // def sum(n: Int) = if (n == 0) { 0 } else { n + sum(n-1) } def sum(n: Int): Int = if (n == 0) { 0 } else { n + sum(n - 1) } println(sum(10)) // 印出 55 最後,在 Scala 當中函式內是可以繼續再定義函式的,舉例來說我們可以用累加器的方式來實作上述的遞回函式: def sum(n: Int, accumulator: Int): Int = { if (n == 0) { accumulator } else { sum(n-1, accumulator + n) } } println (sum(10, 0)) // 印出 55 但這樣一來,使用者在呼叫的時候就必須傳遞兩個參數,而且第二個參數理論上一定要是零,把它曝露給使用者似乎不太合理。在這個時候,我們就可以藉助巢狀函式,將第二個參數封裝起來,不讓使用者接觸到: def sum(n: Int) = { def sumImpl(n: Int, accumulator: Int): Int = { if (n == 0) { accumulator } else { sumImpl(n-1, accumulator + n) } } sumImpl(n, 0) } println (sum(10)) // 印出 55 到底什麼是 Higher-Order Function前面囉囉嗦嗦了那麼多,我們終於進入到 Scala 程式語言中比較有趣,可能也是讀者可能比較不熟悉的部份--用來支援 Functional Programming 編程典範的工具。 若讀者曾經稍微接觸過 Functional Programming 或看到相關的介紹的話,那麼可能已經聽過「first-class function」和「Higher-order function」這些東西,而這一小節我們要介紹的就是 Higher-order function。 話說筆者第一次接觸 Functional Programming 時,看的是其他語言的介紹(例如 Haskell 或 Ruby),但對於這些概念始終都無法掌握和理解,直到學習了 Scala 之後,才發現原來 Functional Programming 中的東西,也都是可以從物件導向的概念去理解的。 所以如果您和筆者一樣,對於 Functional Programming 有興趣,但卻始終格格不入,那麼從 Scala 開始學習是一個不錯的方式--因為 Scala 實際上就是用物件導向的方式來實作 Functional Programming 的,我們可以用比較熟悉的角度來切入。 回到正題,當我們提到 first-class function 和 Higher-order function 是,其實代表的是這樣一件事--函式要求的參數可以是另一個函式,而函式可以被當作參數傳遞。 啥?參數是函式,函式可以傳來傳去?這是什麼東西?!別緊張,其實這種概念我們在物件導向的程式裡也是常常用的,特別是在 GUI 介面的程式裡。 舉例來說,在 Android 裡面我們要讓一個按鈕被按下去後執行特定的工作,可以用 final Button button = (Button) findViewById(R.id.button_id); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Perform action on click } }); 但不知道你有沒有想過,為什麼這個 沒錯,這裡就是 First-class function 和 Higer-order function 出場的地方了!既然這個物件裡只有一個方法,乾脆就省略 new 這些物件和覆寫的步驟,直接把函式傳進去吧! 所以在 Scala 中我們可以定義一個函式,他的參數是另一個函式: def onClick(callback: (Int, Long) => Any) = { val buttonID = 1 // 假定這個 buttonID 會設定成按鈕的 ID val currentTime = System.currentTimeMillis // onClick 函式被呼叫的時間 callback(buttonID, currentTime) } 如果先不去看 至於呼叫 callback 執行後會發生什麼事情,在這個時點我們不曉得,因為就如同在 GUI 中按鈕被按下去的行為是由 Listener 所控制,onClick 的行為也是由 callback 這個參數所決定的。 接下來我們再來看 如果我們要使用 // FirstClassFunction.scala val myCallback = (buttonID: Int, time: Long) => { println("buttonID:" + buttonID) println("time:" + time) } val myCallback2 = (buttonID: Int, time: Long) => { println("I will do something...") } def onClick(callback: (Int, Long) => Any) = { val buttonID = 1 // 假定這個 buttonID 會設定成按鈕的 ID val currentTime = System.currentTimeMillis // onClick 函式被呼叫的時間 callback(buttonID, currentTime) } onClick(myCallback) onClick(myCallback2) 在這個例子中,我們呼叫了 onClick 兩次,並且傳入不同的 first-class function 給他,所以兩次 onClick 的執行結果自然也不同: brianhsu@VaioPro13 ~ $ scala FirstClassFunction.scala buttonID:1 time:1402291789247 I will do something... 當然,我們也可以不將 first-class function 放入變數中,而是直接將其當做參數傳入函式中。其中要注意的是當使用這種方式的時候,由於 Scala 已經知道 onClick 中的 callback 需要的是一個接受一個 Int 與一個 Long 的函式,所以我們可以省略 buttonID 與 currentTime 的型別。 def onClick(callback: (Int, Long) => Any) = { val buttonID: Int = 1 val currentTime = System.currentTimeMillis callback(buttonID, currentTime) } onClick((buttonID: Int, time: Long) => println("buttonID:" + buttonID) ) onClick((buttonID, time) => println("buttonID:" + buttonID) ) 看到這邊讀者可能會有疑惑,為什麼筆者之前會說這樣的概念可以用物件導向的方式理解,而 Scala 實際上也是用物件導向的方式來實作呢? 這是因為下面這兩點:
由於 Scala 編譯器只是做了一些語法上的轉換,所以我們上面的程式碼可以改寫成下面這樣,其中 myCallback 使用 Scala 提供的簡便語法,而 myCallback2 和 onClick 則直接使用 Function2 來實作。 // FirstClassFunction2.scala val myCallback = (buttonID: Int, time: Long) => { println("buttonID:" + buttonID) println("time:" + time) } val myCallback2 = new Function2[Int, Long, Any] { def apply(buttonID: Int, time: Long): Any = { println("I will do something...") } } def onClick(callback: Function2[Int, Long, Any] ) = { val buttonID = 1 // 假定這個 buttonID 會設定成按鈕的 ID val currentTime = System.currentTimeMillis // onClick 函式被呼叫的時間 callback.apply(buttonID, currentTime) } onClick(myCallback) onClick(myCallback2) 此外,Scala 也提供了一個工具,讓我們可以觀察 Scala 是怎麼樣實作出 Java 沒有的功能的,只要在編譯的時候使用 -print 選項,Scala 就會印出只使用 Java 有的功能的實作版本,當我們不確定 Scala 的程式碼到底是怎麼實作的時候,可以使用這個選項來確認。 除了使用 first-class function 的語法之外,我們也可以先定義一般的函式,再使用其名稱加上底線符號 def myCallback(buttonID: Int, currentTime: Long) = { println("ButtonID:" + buttonID) } def onClick(callback: Function2[Int, Long, Any] ) = { val buttonID = 1 // 假定這個 buttonID 會設定成按鈕的 ID val currentTime = System.currentTimeMillis // onClick 函式被呼叫的時間 callback.apply(buttonID, currentTime) } onClick(myCallback _) // 直接使用提升一般的函式成為 first-class function val myCallback2 = myCallback _ // 提升過後的函式和 first-class function 一樣可以指定給變數 onClick(myCallback2) 小結在這一篇中,我們介紹了在 Scala 中要如何宣告變數與函式,並且也看到了 Scala 是如何使用物件導向再加上一點編譯器的語法取代功能,來實作出在其他 Functional Programming 語言中可以將函式做為參數傳遞的功能。 不過到目前為止,我們看到的都還只是基本的語法部份,不過別擔心,從下一篇開始我們將進入 Higher-order function 的實戰,來看看到底要如何在程式碼當中使用這些功能,而這些功能又會為我們帶來什麼好處。 [註一] 這個行為即將在未來版本的 Scala 中 Deprecated 掉,Scala 官方已不建議再使用這樣的語法,而是明確地在每個函式宣告後加上等號,這裡僅是為了讓讀者可以了解目前 Scala 程式碼常出現的函式定議方式而提及。 |