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 程式碼常出現的函式定議方式而提及。 |

Java 學習之路



