在本系列的第一篇文章 《使用遞迴的方式去思考》中,作者並沒有首先介紹 Scala 的語法,這樣做有兩個原因:一是因為過多的陷入語法的細節當中,會分散讀者的注意力,反而忽略了對於基本概念,基本思想的理解;二是因為 Scala 語法非常簡潔,擁有其他語言程式設計經驗的程式設計師很容易讀懂 Scala 程式碼。現在我們將回過頭來,從基本的語法開始學習 Scala 語言。大家會發現 Scala 語言異常精煉,實現同樣功能的程式,在程式碼量上,使用 Scala 實現通常比 Java 實現少一半或者更多。短小精悍的程式碼常常意味著更易懂,更易維護。本文將為大家介紹 Scala 語言的基本語法,幫助大家寫出自己的第一個 Scala 程式。
開發環境
學習 Scala,最方便的方式是安裝一個 Scala 的 IDE(整合開發環境),Typesafe 公司開發了一款基於 Eclipse 的 IDE。該款 IDE 為 Scala 初學者提供了一個方便的功能:Worksheet。像 Python 或者 Ruby 提供的 RELP(Read-Eval-Print Loop)一樣,Worksheet 允許使用者輸入 Scala 表示式,儲存後立即得到程式執行結果,非常方便使用者體驗 Scala 語言的各種特性。如何安裝 Scala IDE 和使用 Worksheet,請大家參考 https://class.coursera.org/progfun-002/wiki/view?page=ToolsSetup。
Hello World
讓我們以經典的 Hello World 程式開始,只需在 Worksheet 裡輸入 println("Hello World!")
儲存即可,在該語句的右邊就可以立刻看到程式執行結果。
Worksheet 也可以被當作一個簡單的計算器,試著輸入一些算式,儲存。
圖 1. Worksheet
變數和函式
當然,我們不能僅僅滿足使用 Scala 來進行一些算術運算。寫稍微複雜一點的程式,我們就需要定義變數和函式。Scala 為定義變數提供了兩種語法。使用 val
定義常量,一經定義後,該變數名不能被重新賦值。使用 var
定義變數,可被重新賦值。在 Scala 中,鼓勵使用 val
,除非你有明確的需求使用 var
。對於 Java 程式設計師來說,剛開始可能會覺得有違直覺,但習慣後你會發現,大多數場合下我們都不需要 var
,一個可變的變數。
清單 1. 定義變數
1 2 3 4 5 6 7 8 9 10 |
val x = 0 var y = 1 y = 2 // 給常量賦值會出現編譯錯誤 // x = 3 // 顯式指定變數型別 val x1: Int = 0 var y1: Int = 0 |
仔細觀察上述程式碼,我們會有兩個發現:
定義變數時沒有指定變數型別。這是否意味著 Scala 是和 Python 或者 Ruby 一樣的動態型別語言呢?恰恰相反,Scala 是嚴格意義上的靜態型別語言,由於其採用了先進的型別推斷(Type Inference)技術,程式設計師不需要在寫程式時顯式指定型別,編譯器會根據上下文推斷出型別資訊。比如變數 x
被賦值為 0,0 是一個整型,所以 x
的型別被推斷出為整型。當然,Scala 語言也允許顯示指定型別,如變數 x1
,y1
的定義。一般情況下,我們應儘量使用 Scala 提供的型別推斷系統使程式碼看上去更加簡潔。
另一個發現是程式語句結尾沒有分號,這也是 Scala 中約定俗成的程式設計習慣。大多數情況下分號都是可省的,如果你需要將兩條語句寫在同一行,則需要用分號分開它們。
函式的定義也非常簡單,使用關鍵字 def
,後跟函式名和引數列表,如果不是遞迴函式可以選擇省略函式返回型別。Scala 還支援定義匿名函式,匿名函式由引數列表,箭頭連線符和函式體組成。函式在 Scala 中屬於一級物件,它可以作為引數傳遞給其他函式,可以作為另一個函式的返回值,或者賦給一個變數。在下面的示例程式碼中,定義的匿名函式被賦給變數 cube
。匿名函式使用起來非常方便,比如 List
物件中的一些方法需要傳入一個簡單的函式作為引數,我們當然可以定義一個函式,然後再傳給 List
物件中的方法,但使用匿名函式,程式看上去更加簡潔。
清單 2. 定義函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 定義函式 def square(x: Int): Int = x * x // 如果不是遞迴函式,函式返回型別可省略 def sum_of_square(x: Int, y: Int) = square(x) + square(y) sum_of_square(2, 3) // 定義匿名函式 val cube = (x: Int) => x * x *x cube(3) // 使用匿名函式,返回列表中的正數 List(-2, -1, 0, 1, 2, 3).filter(x => x > 0) |
讓我們再來和 Java 中對應的函式定義語法比較一下。首先,函式體沒有像 Java 那樣放在 {}
裡。Scala 中的一條語句其實是一個表示式,函式的執行過程就是對函式體內的表示式的求值過程,最後一條表示式的值就是函式的返回值。如果函式體只包含一條表示式,則可以省略 {}
。其次,沒有顯示的 return
語句,最後一條表示式的值會自動返回給函式的呼叫者。
和 Java 不同,在 Scala 中,函式內部還可以定義其他函式。比如上面的程式中,如果使用者只對 sum_of_square 函式感興趣,則我們可以將 square 函式定義為內部函式,實現細節的隱藏。
清單 3. 定義內部函式
1 2 3 4 5 |
def sum_of_square(x: Int, y: Int): Int = { def square(x: Int) = x * x square(x) + square(y) } |
流程控制語句
複雜一點的程式離不開流程控制語句,Scala 提供了用於條件判斷的 if else
和表示迴圈的 while
。和 Java 中對應的條件判斷語句不同,Scala 中的 if else
是一個表示式,根據條件的不同返回相應分支上的值。比如下面例子中求絕對值的程式,由於 Scala 中的 if else
是一個表示式,所以不用像 Java 那樣顯式使用 return
返回相應的值。
清單 4. 使用 if else 表示式
1 2 |
def abs(n: Int): Int = if (n > 0) n else -n |
和 Java 一樣,Scala 提供了用於迴圈的 while 語句,在下面的例子中,我們將藉助 while 迴圈為整數列表求和。
清單 5. 使用 while 為列表求和
1 2 3 4 5 6 7 8 9 |
def sum(xs: List[Int]) = { var total = 0 var index = 0 while (index < xs.size) { total += xs(index) index += 1 } total } |
上述程式是習慣了 Java 或 C++ 的程式設計師想到的第一方案,但仔細觀察會發現有幾個問題:首先,使用了 var
定義變數,我們在前面說過,儘量避免使用 var
。其次,這個程式太長了,第一次拿到這個程式的人需要對著程式仔細端詳一會:程式首先定義了兩個變數,並將其初始化為 0
,然後在 index
小於列表長度時執行迴圈,在迴圈體中,累加列表中的元素,並將 index
加 1
,最後返回最終的累加值。直到這時,這個人才意識到這個程式是對一個數列求和。
讓我們換個角度,嘗試用遞迴的方式去思考這個問題,對一個數列的求和問題可以簡化為該數列的第一個元素加上由後續元素組成的數列的和,依此類推,直到後續元素組成的數列為空返回 0。具體程式如下,使用遞迴,原來需要 9 行實現的程式現在只需要兩行,而且程式邏輯看起來更清晰,更易懂。(關於如何使用遞迴的方式去思考問題,請參考作者的另外一篇文章《使用遞迴的方式去思考》)
清單 6. 使用遞迴對數列求和
1 2 3 4 |
//xs.head 返回列表裡的頭元素,即第一個元素 //xs.tail 返回除頭元素外的剩餘元素組成的列表 def sum1(xs: List[Int]): Int = if (xs.isEmpty) 0 else xs.head + sum1(xs.tail) |
有沒有更簡便的方式呢?答案是肯定的,我們可以使用列表內建的一些方法達到同樣的效果:
1 |
xs.foldLeft(0)((x0, x) => x0 + x) |
該方法傳入一個初始值 0,一個匿名函式,該匿名函式累加列表中的每一個元素,最終返回整個列表的和。使用上面的方法,我們甚至不需要定義額外的方法,就可以完成同樣的操作。事實上,List 已經為我們提供了 sum 方法,在實際應用中,我們應該使用該方法,而不是自己定義一個。作者只是希望通過上述例子,讓大家意識到 Scala 雖然提供了用於迴圈的 while 語句,但大多數情況下,我們有其他更簡便的方式能夠達到同樣的效果。
使用牛頓法求解平方根
掌握了上面這些內容,我們已經可以利用 Scala 求解很多複雜的問題了。比如我們可以利用牛頓法定義一個函式來求解平方根。牛頓法求解平方根的基本思路如下:給定一個數 x
,可假設其平方根為任意一個正數 ( 在這裡,我們選定 1 為初始的假設 ),然後比較 x
與該數的平方,如果兩者足夠近似(比如兩者的差值小於 0.0001),則該正數即為 x
的平方根;否則重新調整假設,假設新的平方根為上次假設
與 x/ 上次假設
的和的平均數。通過下表可以看到,經過僅僅 4 次迭代,就能求解出相當精確的 2 的平方根。
表 1. 牛頓法求解 2 的平方根
假設 | 假設的平方與 2 進行比較 | 新的假設 |
---|---|---|
1 | |1 * 1 – 2| = 1 | (1 + 2/1)/2 = 1.5 |
1.5 | |1.5 * 1.5 – 2| = 0.25 | (1.5 + 2/1.5)/2 = 1.4167 |
1.4167 | |1.4167 * 1.4167 – 2| = 0.0070 | (1.4167 + 2/1.4167)/2 = 1.4142 |
1.4142 | |1.4142 * 1.4142 – 2| = 0.000038 | …… |
將上述演算法轉化為 Scala 程式,首先我們定義這個迭代過程,這也是該演算法的核心部分,所幸這一演算法非常簡單,利用遞迴,一個 if else
表示式就能搞定。後續為兩個輔助方法,讓我們的程式看起來更加清晰。最後我們選定初始假設為 1
,定義出最終的 sqrt
方法。
清單 7. 使用牛頓法求解平方根
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 迭代函式,若解不滿足精度,通過遞迴呼叫接續迭代 def sqrtIter(guess: Double, x: Double): Double = if (isGoodEnough(guess, x)) guess else sqrtIter((guess + x / guess)/2, x) // 判斷解是否滿足要求 def isGoodEnough(guess: Double, x: Double) = abs(guess * guess - x)< 0.0001 // 輔助函式,求絕對值 def abs(x: Double) = if (x < 0) -x else x // 目標函式 def sqrt(x: Double): Double = sqrtIter(1, x) // 測試程式碼 sqrt(2) |
這段程式看起來相當優美:首先它沒有使用 var
定義其他輔助變數,在程式中避免使用 var
總是一件好事情;其次它沒有使用 while
迴圈描述整個迭代過程,取而代之的是一段非常簡潔的遞迴,使程式邏輯上看起來更加清晰;最後它沒有將整個邏輯全部塞到一個函式裡,而是分散到不同的函式裡,每個函式各司其職。然而這段程式也有一個顯而易見的缺陷,作為使用者,他們只關心 sqrt
函式,但這段程式卻將其他一些輔助函式也暴露給了使用者,我們在前面提到過,Scala 裡可以巢狀定義函式,我們可以將這些輔助函式定義為sqrt
的內部函式,更進一步,由於內部函式可以訪問其函式體外部定義的變數,我們可以去掉這些輔助函式中的 x
引數。最終的程式如下:
清單 8. 使用牛頓法求解平方根 – 使用內部函式隱藏細節
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 目標函式,通過將需要用到的輔助函式定義為內部函式,實現細節的隱藏 def sqrt(x: Double): Double = { // 迭代函式,若解不滿足精度,通過遞迴呼叫接續迭代 def sqrtIter(guess: Double): Double = if (isGoodEnough(guess)) guess else sqrtIter((guess + x / guess) / 2) // 判斷解是否滿足要求 def isGoodEnough(guess: Double) = abs(guess * guess - x) < 0.0001 // 輔助函式,求絕對值 def abs(x: Double) = if (x < 0) -x else x sqrtIter(1) } |
如何執行 Scala 程式
我們已經利用 Scala 整合開發環境提供的 Worksheet 體驗了 Scala 的基本語法,在實際開發中,我們更關心如何執行 Scala 程式。在執行方式上,Scala 又一次體現出了它的靈活性。它可以被當作一種指令碼語言執行,也可以像 Java 一樣,作為應用程式執行。
作為指令碼執行
我們可以將 Scala 表示式寫在一個檔案裡,比如 Hello.scala。在命令列中直接輸入 scala Hello.scala
就可得到程式執行結果。
清單 9. Hello.scala
1 |
println(“Hello Script!”) |
作為應用程式執行
作為應用程式執行時,我們需要在一個單例物件中定義入口函式 main
,經過編譯後就可以執行該應用程式了。
清單 10. HelloWorld.scala
1 2 3 4 5 |
object HelloWorld { def main(args: Array[String]): Unit = { println("Hello World!") } } |
Scala 還提供了一個更簡便的方式,直接繼承另一個物件 App,無需定義 main
方法,編譯即可執行。
清單 11. HelloScala.scala
1 2 3 |
object HelloScala extends App { println("Hello Scala!") } |
結束語
本文為大家介紹了 Scala 的基本語法,相比 Java,Scala 的語法更加簡潔,比如 Scala 的型別推斷可以省略程式中絕大多數的型別宣告,短小精悍的匿名函式可以方便的在函式之間傳遞,還有各種在 Scala 社群約定俗成的習慣,比如省略的分號以及函式體只有一條表示式時的花括號,這一切都幫助程式設計師寫出更簡潔,更優雅的程式。限於篇幅,本文只介紹了 Scala 最基本的語法,如果讀者想跟進一步學習 Scala,請參考 Scala 的 官方文件及文末所附的參考資源。
掌握了這些基本的語法,我們將在下一篇文章中為大家介紹如何使用 Scala 進行函數語言程式設計,這是 Scala 最讓人心動的特性之一,對於習慣了物件導向的程式設計師來說,學習 Scala 更多的是在學習如何使用 Scala 進行函數語言程式設計。