[翻譯] 給Java程式設計師的Scala入門教學

看板Translate-CS作者 ( )時間11年前 (2013/10/22 14:51), 編輯推噓0(000)
留言0則, 0人參與, 最新討論串1/1
譯自 http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html 因為原文是用markdown撰寫,譯文也直接用markdown格式撰寫 github好讀版(?) https://github.com/chikei/scala.github.com/blob/zh_TW/zh/tutorials/ scala-for-java-programmers.md ~~~正文分隔線~~~ ## 介紹 此教學將對Scala語言以及編譯器做一個簡易的介紹。設定的讀者為具有程設經驗且想 要看Scala功能概要的人。內文假設讀者有著基本、特別是Java上的物件導向程設知識。 ## 第一個例子 這邊用標準的 *Hello world* 程式作為第一個例子。雖然它很無趣,可是這讓我們在 僅用少量語言下演示Scala工具。程式如下: object HelloWorld { def main(args: Array[String]) { println("Hello, world!") } } Java程式員應該對這個程式的結構感到熟悉:有著一個 `main` 函式,該函式接受一 個字串陣列引數,也就是命令列引數;函式內容為呼叫已定義好的函式 `println` 並 用Hello world字串當引數。 `main` 函式沒有回傳值(它是程序函式)。因此並不需要 宣告回傳型別。 Java程式員不太熟悉的是包著 `main` 函式的 `object` 宣告。這種宣告引入我們一 般稱之 *Singleton* 的東西,也就是只有一個實體的類別。所以上面的宣告同時宣告 了一個 `HelloWorld` 類別跟一個這類別的實體,也叫做 `HelloWorld`。該實體會在 第一次被使用到的時候即時產生。 眼尖的讀者可能已經注意到這邊 `main` 函式的宣告沒有帶著 `static`。這是因為 Scala沒有靜態成員(函式或資料欄)。Scala程式員將這成員宣告在單實例物件中,而 不是定義靜態成員。 ### 編譯這例子 我們用Scala編譯器 `scalac`來編譯這個例子。`scalac` 就像大多數的編譯器一樣, 它接受原碼檔當引數,並接受額外的選項,然後產生一個或多個物件檔。它產出的物 件檔為標準的Java class檔案。 如果我們將上面的程式存成 `HelloWorld.scala` 檔,編譯的指令為( `>` 是提示字 元,不用打): > scalac HelloWorld.scala 這會在現在的目錄產生一些class檔案。其中一個會叫做 `HelloWorld.class`,裡面 包含著可被 `scala` 直接執行的類別。 ### 執行範例 一旦編譯過後,Scala程式可以用 `scala` 指令執行。它的使用方式非常的像執行 Java程式的 `java` 指令,並且接受同樣的選項。上面的範例可以用以下的指令來執 行並得到我們預期的輸出: > scala -classpath . HelloWorld Hello, world! ## 與Java互動 Scala的優點之一是它非常的容易跟Java程式碼溝通。預設匯入所有 `java.lang` 底 下之類別,其他類別則需要明確匯入。 讓我們看個展示這點的範例。取得現在的日期並根據某個特定的國家排版成該國的格 式,如法國。 Java的標準函式庫定義了一些有用的工具類別,如 `Date` 跟 `DateFormat`。因為 Scala可以無縫的跟Java互動,這邊不需要以Scala實作同樣的類別--我們只需要匯入 對應的Java套件: import java.util.{Date, Locale} import java.text.DateFormat import java.text.DateFormat._ object FrenchDate { def main(args: Array[String]) { val now = new Date val df = getDateInstance(LONG, Locale.FRANCE) println(df format now) } } Scala的匯入陳述式跟Java的非常像,但更為強大。如第一行,同一個package下的多 個類別可以用大括號括起來一起導入。另外一個差別是,當要匯入套件或類別下所有 的名稱時,用下標(`_`)而不是星號(`*`)。這是因為星號是一個合法的Scala識別符號 (如函式名稱)。 所以第三行的陳述式導入所有 `DateFormat` 類別的成員。這讓靜態函式 `getDateInstance` 跟靜態資料欄 `LONG` 可直接被使用。 在 `main` 函式中我們先創造一個Java的 `Date` 類別實體,該實體預設擁有現在的 日期。接下來用 `getDateInstance` 函式定義日期格式。最後根據地區化的 `DateFormat` 實體對現在日期排版格式化並印出。最後一行展現了一個Scala有趣的 特點。只需要一個引數的函式可以用中綴語法呼叫。就是說,這個表示式 df format now 是比較不詳細版本的這個表示式 df.format(now) 這點也許看起來只是一個小小的語法細節,但是他有著重要的後果,其中一個將會在 下一節做介紹。 讓我們以,Scala可以直接繼承Java類別跟實作Java介面,來為這節做結尾。 ## 萬物皆物件 Scala是一個純粹的物件導向語言,這句話的意思是說,*所有東西*都是物件,包括數 字、函式。因為Java將基本型別跟參照型別分開,而且沒有辦法像操作變數一樣操作 函式,從這角度來看Scala跟Java是不同的。 ### 數字是物件 因為數字是物件,他們也有函式。事實上,一個像底下的算數表示式: 1 + 2 * 3 / x 只有使用函式呼叫,因為像前一節一樣,該式等價於 (1).+(((2).*(3))./(x)) 這也表示著 `+`、`*` 之類的在Scala裡是合法的識別符號。 因為Scala的詞法分析器對於符號採用最長匹配,在第二版的表示式當中,那些括號是 必要的。也就是說分析器會把這個表示式: 1.+(2) 拆成 `1.`、`+`、`2` 這三個符號。會這樣拆分是因為 `1.` 既是合法匹配同時又比 `1` 長。 `1.` 會被解釋成文字 `1.0`,使得他被視為 `Double` 而不是 `Int`。把 表示式寫成: (1).+(2) 可以避免 `1` 被解釋成 `Double`。 ### 函式是物件 可能令Java程式員更為驚訝的會是,Scala中函式也是物件。因此,將函式當做引數傳 遞、把它們存入變數、從其他函式返回函式都是可能的。能夠像操作變數一樣的操作 函式這點是*函數編程*這一非常有趣的程設典範的基石之一。 為何把函式當做變數一樣的操作會很有用呢,讓我們考慮一個定時函式,它的功能是 每秒執行一些動作。我們要怎麼將這動作傳給它?最直接的便是將這動作視為函式傳 入。應該有不少的程式員對這種簡單傳遞函式的行為很熟悉:通常在使用者介面相關 的程式上,用以註冊一些當事件發生時被呼叫的回呼函式。 在接下來的程式中,定時函式叫做 `oncePerSecond` ,它接受一個回呼函式做參數。 該函式的型別被寫作 `() => Unit` ,這個型別便是所有無引數且無返回值的函式的 型別( `Unit` 這個型別就像是C/C++的 `void` )。此程式的主函式只是呼叫定時函式 並帶入回呼函式,回呼函式輸出一句話到終端上。也就是說這個程式會不斷的每秒輸 出一次"time flies like an arrow"。 object Timer { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def timeFlies() { println("time flies like an arrow...") } def main(args: Array[String]) { oncePerSecond(timeFlies) } } 值得注意的是,這邊輸出的時候我們使用Scala的函式 `println`,而不是 `System.out` 裡的函式。 #### 匿名函式 這程式還有改進的空間。第一點,函式 `timeFlies` 只是為了能夠被傳遞進 `oncePerSecond` 而定義的。賦予一個只被使用一次的函式名字似乎是沒有必要的, 最好能夠在傳入 `oncePerSecond` 時構造出這個函式。Scala可以藉由*匿名函式*來 達到這點。利用匿名函式的改進版本程式如下: object TimerAnonymous { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def main(args: Array[String]) { oncePerSecond(() => println("time flies like an arrow...")) } } 這例子中的右箭頭 `=>` 告訴我們有一個匿名函式,右箭頭將函式引數跟函式內容分 開。這個例子中,在箭頭左邊那組空的括號告訴我們引數列是空的。函式內容則是跟 先前的 `timeFlies` 裡一樣。 ## 類別 之前已講過,Scala是一個物件導向語言,因此它有著類別的概念。(更精確的說,的 確有一些物件導向語言沒有類別的概念,但是Scala不是這類)。Scala宣告類別的語法 跟Java很接近。一個重要的差別是,Scala的類別可以有參數。這邊用底下複數的定義 來展示: class Complex(real: Double, imaginary: Double) { def re() = real def im() = imaginary } 這個複數類別接受兩個參數,分別為實跟虛部。在創造 `Complex` 的實體時,必須傳 入這些參數: `new Complex(1.5, 2.3)`。這個類別有兩個函式分別叫做 `re` 跟 `im` 讓我們取得這兩個部分。 值得注意的是,這兩個函式的回傳值並沒有被明確的給定。編譯器將會自動的推斷, 它會查看這些函式的右側並推導出這兩個函式都會回傳型別為 `Double` 的值。 編譯器並不一定每次都能夠推斷出型別,而且很不幸的是我們並沒有簡單的規則分辨 哪種情況能推斷,哪種情況不能。因為當編譯器無法推斷未明確給定的型別時它會回 報錯誤,實務上這通常不是問題。Scala的初學者在遇到那些看起來很簡單就能推導出 型別的情況時,應該嘗試著忽略型別宣告並看看編譯器是不是也覺得可以推斷。多嘗 試幾次之後程式員應該能夠體會到何時忽略型別、何時該明確指定。 ### 無引數函式 函式 `re`、`im` 有個小問題,為了呼叫函式,我們必須在函式名稱後面加上一對空 括號,如這個例子: object ComplexNumbers { def main(args: Array[String]) { val c = new Complex(1.2, 3.4) println("imaginary part: " + c.im()) } } 最好能夠在不需要加括號的情況下取得實虛部,這樣便像是在取資料欄。Scala完全可 以做到這件事,需要的只是在定義函式的時候*不要定義引數*。這種函式跟零引數函 式是不一樣的,不論是定義或是呼叫,它們都沒有括號跟在名字後面。我們的 `Complex` 可以改寫成: class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary } ### 繼承與覆寫 Scala中所有的類別都繼承自一個母類別。像前一節的 `Complex` 這種沒有指定的例 子,Scala會暗中使用 `scala.AnyRef`。 Scala中可以覆寫繼承自母類別的函式。但是為了避免意外的覆寫,必須加上 `override` 修飾字來明確表示要覆寫函式。我們以覆寫 `Complex` 類別中來自 `Object` 的 `toString` 作為範例。 class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary override def toString() = "" + re + (if (im < 0) "" else "+") + im + "i" } ## Case Class跟模式匹配(pattern matching) 樹是常見的資料結構。如:解譯器跟編譯器內部常見的表示程式方式便是樹;XML文件 是樹;還有一些容器是根基於樹,如紅黑樹。 接下來我們會藉由一個小型計算機程式來看看Scala是如何呈現並操作樹。這個程式的 功能將會是足以操作簡單、僅含有整數、常數、變數跟加法的算術式。`1+2` 跟 `(x+x)+(7+y)`為兩個例子。 我們得先決定這種表示式的表示法。最自然表示法便是樹,其中節點是操作、葉點是 值。 Java中我們會將這個樹用一個抽象母類別表示,然後每種節點跟葉點分別有各自的實 際類別。在函數邊程裡會用代數資料類型。Scala則是提供了介於兩者之間的 *case class*。將它運用在這邊會是如下: abstract class Tree case class Sum(l: Tree, r: Tree) extends Tree case class Var(n: String) extends Tree case class Const(v: Int) extends Tree `Sum`、`Var`、`Const` 類別定義成case class代表著它們跟一般類別有所差別: - 在創建類別實體時不需要用 `new`(也就是說我們可以寫 `Const(5)`,而不是 `new Const(5)`)。 - 對應所有的建構式參數,Scala會自動定義對應的取值函式(即,對於 `Const` 類別 的實體,我們可以直接用 `c.v` 來取得建構式中的 `v` 參數)。 - `equals` 跟 `hashCode` 會有預設的定義。該定義會根據實體的*結構*而不是個別 實體的識別來運作。 - `toString` 會有預設的定義。會印出"原始型態"(即,`x+1` 的樹會被印成 `Sum(Var(x),Const(1))`)。 - 這些類別的實體可以藉由*模式匹配*來拆解。 現在我們有了算術表示式的資料型別,可以開始定義各種運算。我們將從一個可以在 *環境*內對運算式求值的函式起頭。環境的用處是賦值給變數。舉例來說,運算式 `x+1` 在一個將 `x` 賦與 `5` 的環境(寫作 `{ x -> 5 }` )下求值會得到 `6`。 因此我們需要一個表示環境的方法。當然我們可以用一些像是雜湊表的關連性資料結 構,但是我們也可以直接用函式!環境就只是一個將值對應到(變數)名稱的函式。之 前提到的環境 `{ x -> 5 }` 在Scala中可以簡單的寫作: { case "x" => 5 } 這串符號定義了一個當輸入是字串 `"x"` 的時候回傳整數 `5`,其他輸入則是用例外 表示失敗的函式。 開始實作之前,讓我們先給環境型別一個名字。當然,我們可以直接用 `String => Int`,但是給這型別名字可以讓我們簡化程式,而且在未來要改動的時 候較為簡便。在Scala我們是這樣表示這件事: type Environment = String => Int 於是型別 `Environment` 便可以當做輸入 `String` 回傳 `Int` 函式的型別的代名。 現在我們可以給出求值函式的實作。概念上非常的簡單:兩個表示式和的值是兩個表 示式值的和;變數的值直接從環境取值;常數的值就是常數本身。表示這些在Scala裡 並不困難: def eval(t: Tree, env: Environment): Int = t match { case Sum(l, r) => eval(l, env) + eval(r, env) case Var(n) => env(n) case Const(v) => v } 這個求值函式藉由對樹 `t` 做*模式匹配*來求值。上述實作的意思應該從直觀上便很 明確: 1. 首先檢查樹 `t` 是否為 `Sum`,如果是的話將左/右側子樹綁定到新變數 `l`/`r` ,然後再對箭頭後方的表示式求值;這一個表示式可以使用(而且這邊也用到)根據 箭頭左側模式所綁定的變數,也就是 `l` 跟 `r`, 2. 如果第一個檢查失敗,也就是說樹不是 `Sum`,接下來檢查 `t` 是否為 `Var`, 如果是的話將 `Var` 所帶的名稱綁定到變數 `n` 並求值右側的表示式, 3. 如果第二個檢查也失敗,表示樹不是 `Sum` 也不是 `Var`,那便檢查是不是 `Const`,如果是的話將 `Const` 所帶的名稱綁定到變數 `v` 並求值右側的表 示式, 4. 最後,如果全部的檢查都失敗,會丟出例外表示匹配失敗;這只會在有更多 `Tree` 的子類別的情況下發生。 如上,模式匹配基本上就是嘗試將一個值對一系列的模式做匹配,並在一個模式成功 的匹配時抽取並命名該值的各部分,最後對一些程式碼求值,而這些程式碼通常會利 用被命名到的部位。 一個經驗豐富的物件導向程式員也許會疑惑為何我們不將 `eval` 定義成 `Tree` 類 別跟子類的*函式*。由於Scala允許在case class中跟一般的類別一樣定義函式,事實 上我們可以這樣做。要用模式匹配或是函式只是品味的問題,但是這會對擴充性有重 要的影響。 - 當使用函式的時候,只要定義新的 `Tree` 子類便新增新的節點,相當的容易。另 一方面,增加新的操作需要修改所有的子類,很麻煩。 - 當使用模式匹配的時候情況則反過來:增加新節點需要修改所有對樹做模式匹配的 函式將新節點納入考慮;增加新的操作則很簡單,定義新的函式就好。 讓我們定義新的操作以更進一步的探討模式匹配:對符號求導數。讀者們可能還記得 這個操作的規則: 1. 和的導數是導數的和 2. 如果是對變數 `v` 取導數,變數 `v` 的導數是1,不然就是0 3. 常數的導數是0 這些規則幾乎可以從字面上直接翻成Scala程式碼: def derive(t: Tree, v: String): Tree = t match { case Sum(l, r) => Sum(derive(l, v), derive(r, v)) case Var(n) if (v == n) => Const(1) case _ => Const(0) } 這個函式引入兩個關於模式匹配的新觀念。首先,變數的 `case` 運算式有一個 *看守*,也就是 `if` 關鍵字之後的表示式。除非表示式求值為真,不然這個看守會 讓匹配直接失敗。在這邊是用來確定我們只在取導數變數跟被取導數變數名稱相同時 才回傳常數 `1`。第二個新特徵是可以匹配任何值的*萬用字元* `_`。 我們還沒有探討完模式匹配的全部功能,不過為了讓這份文件保持簡短,先就此打住 。我們還是希望能看到這兩個函式在真正的範例如何作用。因此讓我們寫一個簡單的 `main` 函數,對表示式 `(x+x)+(7+y)` 做一些操作:先在環境 `{ x -> 5, y -> 7 }` 下計算結果,然後在對 `x` 接著對 `y` 取導數。 def main(args: Array[String]) { val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y"))) val env: Environment = { case "x" => 5 case "y" => 7 } println("Expression: " + exp) println("Evaluation with x=5, y=7: " + eval(exp, env)) println("Derivative relative to x:\n " + derive(exp, "x")) println("Derivative relative to y:\n " + derive(exp, "y")) } 執行這程式,得到預期的輸出: Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y))) Evaluation with x=5, y=7: 24 Derivative relative to x: Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0))) Derivative relative to y: Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1))) 研究這輸出我們可以發現,取導數的結果應該在輸出前更進一步的化簡。用模式匹配 實作一個基本的化簡函數是一個很有趣(但是意外的棘手)的問題,在這邊留給讀者當 練習。 ## 特質(Traits) 除了由母類別繼承行為以外,Scala類別還可以從一或多個*特質*導入。 對一個Java程式員最簡單去理解特質的方式應該是視他們為帶有實作的介面。在Scala 裡,當一個類別繼承特質時,他實作了該特質的介面並繼承所有特質帶有的功能。 為了理解特質的用處,讓我們看一個經典範例:有序物件。大部分的情況下,一個類 別所產生出來的物件之間可以互相比較大小是很有用的,如排序他們。在Java裡可比 較大小的物件實作 `Comparable` 介面。在Scala中藉由定義等價於 `Comparable` 的特質 `Ord`,我們可以做的比Java稍微好一點。 當在比較物件的大小時,有六個有用且不同的謂詞(predicate):小於、小於等於、等 於、不等於、大於等於、大於。但是把六個全部都實作很煩,尤其是當其中有四個可 以用剩下兩個表示的時候。也就是說,(舉例來說)只要有等於跟小於謂詞,我們就可 以表示其他四個。在Scala中這些觀察可以很漂亮的用下面的特質宣告呈現: trait Ord { def < (that: Any): Boolean def <=(that: Any): Boolean = (this < that) || (this == that) def > (that: Any): Boolean = !(this <= that) def >=(that: Any): Boolean = !(this < that) } 這份定義同時創造了一個叫做 `Ord` 的新型別,跟Java的 `Comparable` 介面有著同 樣的定位,且給了一份以第一個抽象謂詞表示剩下三個謂詞的預設實作。因為所有的 物件預設都有一份等於跟不等於的謂詞,這邊便沒有定義。 上面使用了一個 `Any` 型別,在Scalla中這個型別是所有其他型別的母型別。因為它 同時也是基本型別如 `Int`、`Float`的母型別,可以將其視為更為一般化的Java `Object` 型別。 因此只要定義測試相等性跟劣性的謂詞,並且加入 `Ord`,就可以讓一個類別的物件 們互相比較大小。讓我們實作一個表示陽曆日期的 `Date` 類別來做為例子。這種日 期是由日、月、年組成,我們將用整數來表示這三個資料。因此我們可以定義 `Date` 類別為: class Date(y: Int, m: Int, d: Int) extends Ord { def year = y def month = m def day = d override def toString(): String = year + "-" + month + "-" + day 這邊要注意的是宣告在類別名稱跟參數之後的 `extends Ord`。這個語法宣告了 `Date` 繼承 `Ord` 特質。 然後我們重新定義來自 `Object` 的 `equals` 函式好讓這個類別可以正確的根據每 個資料欄來比較日期。因為在Java中 `equals` 預設實作是直接比較實際物件本身, 並不能在這邊用。於是我們有下面的實作: override def equals(that: Any): Boolean = that.isInstanceOf[Date] && { val o = that.asInstanceOf[Date] o.day == day && o.month == month && o.year == year } 這個函式使用了預定義函式 `isInstanceOf` 跟 `asInstanceOf`。`isInstanceOf` 對應到Java的 `instanceof` 運算子,只在當使用它的物件的型別跟給定型別一樣時 傳回真。 `asInstanceOf` 對應到Java的轉型運算子,如果物件是給定型別的實體, 該物件就會被視為給定型別,不然就會丟出 `ClassCastException` 。 最後我們需要定義測試劣性的謂詞如下。 def <(that: Any): Boolean = { if (!that.isInstanceOf[Date]) error("cannot compare " + that + " and a Date") val o = that.asInstanceOf[Date] (year < o.year) || (year == o.year && (month < o.month || (month == o.month && day < o.day))) } 這邊使用了另外一個預定義函式 `error`,它會丟出帶著給定錯誤訊息的例外。這便 完成了 `Date` 類別。這個類別的實體可被視為日期或是可比較物件。而且他們通通 都定義了之前所提到的六個比較謂詞: `equals`跟`<` 直接出現在類別定義當中,其 他的則是繼承自 `Ord` 特質。 特質在其他場合也有用,不過詳細的探討它們的用途並不在本文件目標內。 ## 泛型 在這份教學裡,我們最後要探討的Scala特性是泛型。Java程式員應該相當的清楚在 Java 1.5之前缺乏泛型所導致的問題。 泛型指的是能夠將型別也作為程式參數的功能。舉例來說,當程式員在為鏈結串列寫 函式庫的時候,他必須決定串列的元素型別為何。由於這串列是要在許多不同的場合 使用,不可能決定串列的元素型別為如 `Int` 一類。這樣限制太多。 Java程式員採用所有物件的母類別 `Object`。這個解決辦法並不理想,一方面這並不 能用在基礎型別(`int`、`long`、`float`之類),再來這表示必須靠程式員手動加入 大量的動態轉型。 Scala藉由可定義泛型類別(跟函式)來解決這問題。讓我們藉由最簡單的類別容器來檢 視這點:參照,它可以是空的或者指向某型別的物件。 class Reference[T] { private var contents: T = _ def set(value: T) { contents = value } def get: T = contents } 類別 `Reference` 帶有一個型別參數 `T`,這個參數會是容器內元素的型別。此型別 被用做 `contents` 變數的型別、 `set` 函式的引數型別、 `get` 函式的回傳型別。 上面的程式碼使用的Scala的變數語法,應該不需要過多的解釋。值得注意的是賦與該 變數的初始值是 `_`,該語法表示預設值。數值型別的預設值是0,`Boolea8n` 型別 是偽, `Unit` 型別是 `()` ,所有的物件型別是 `null`。 為了使用 `Reference` 類型,我們必須指定 `T`,也就是這容器所包容的元素型別。 舉例來說,創造並使用該容器來容納整數,我們可以這樣寫: object IntegerReference { def main(args: Array[String]) { val cell = new Reference[Int] cell.set(13) println("Reference contains the half of " + (cell.get * 2)) } } 如例子中所展現,並不需要先將 `get` 函式所回傳的值轉型便能當做整數使用。同時 因為被宣告為儲存整數,也不可能存除了整數以外的東西到這一個容器中。 ## 結語 本文件對Scala語言做了快速的概覽並呈現一些基本的例子。對Scala有更多興趣的讀 者可以閱讀有更多進階範例的 *Scala By Example*,並在需要的時候參閱 *Scala Language Specification*。 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 211.72.92.133
文章代碼(AID): #1IPY1Xu5 (Translate-CS)
文章代碼(AID): #1IPY1Xu5 (Translate-CS)