Scala Tutorial(1)靜態型別也可以很方便
Java 平台上的非 Java 語言相信大家對於 Java 的相關技術都有所耳聞,話說 Java 程式語言自從 1995 年發展至今,隨著編程典範的不斷地更新,雖然有許多新的功能陸續被加入 Java 程式語言中,但仍然抵擋不住編程者想要有其他更方便使用的程式語言的需求。 而在另一方面,運行 Java 程式所需的 Java 虛擬機器 (Java VM / Java Runtime Enviroment) 卻相當成功,在各種平台與作業系統上幾乎都有相對應的 Java VM 存在。同時 Java 程式開發的生態圈經過了這些年的發展,也累積了相當多的函式庫,幾乎各種常見的應用都有相應的函式庫可用。 在這樣的情境下,開發使用 Java VM 做為執行環境的新程式語言,亦或是將現有的程式語言移植到 Java 平台上,也就不是一件難以理解的事了。 事實上,目前已經有許多其他的程式語言使用 Java VM 做為執行的平台,諸如: 這些語言通常有其各自的開發背景或發展願景,例如 JRuby 與 Jython 是讓 Ruby / Python 等現有的程式語言可以在 Java 平台上執行,Clojure 則是採用了 Lisp 的 code-as-data 的思維典範,Groovy 則試圖提供一個與 Java 語法相似,但是卻又具備動態型別與其他進階功能的程式語言。 而這系列要介紹的 Scala,是一套由 Martin Odersky 設計的程式語言,其目的在於提供一個整合了物件導向與 Functional Programming 這兩編程典範的程式語言,並且同樣使用 Java VM 做為執行的平台。 靜態型別的編譯式語言與目前 Java 平台上常見的非 Java 程式語言相比,Scala 有一個比較特別的地方--它是靜態型別的編譯式程式語言,他的產出是標準的 Java .class 檔。 若讀者曾經學過 Java 程式語言的開發,應該記得如果在僅使用命令列工具的情況下,撰寫並執行一支 HelloWorld.java 的程式,整個流程大致如下: public class HelloWorld { public static void mian(String [] args) { System.out.println("Hello World"); } } // 編譯與執行 // // $ javac HelloWorld.java # 編譯 HelloWorld.java,產生 HelloWorld.class 檔 // $ java HelloWorld # 執行 HelloWorld.class 檔 相較之下,要撰寫一個 object HelloWorld { def main(args: Array[String]) { println("Hello World") } } // 編譯與執行 // $ scalac HelloWorld.java # 編譯 HelloWorld.java // # 會產生 HelloWorld.class 與 HelloWorld$.class 檔 // // $ scala HelloWorld # 執行 HelloWorld.class 檔 // $ java -cp /opt/scala-2.10.3/lib/scala-library.jar:. HelloWorld # 使用 java 執行 Scala 程式 如果先不去理會兩者程式碼內容的不同,我們可以注意到 Scala 的編譯器除了產生的是兩個 事實上,由於 Scala 所編譯出的 靜態型別不一定會很繁瑣對於 Java 程式語言的眾多批評中,很常見的一項就是「因為它是靜態型別的程式語言,所以寫起來很囉嗦」的這個問題,而的確與 Ruby / Python 等動態型別的 Script 語言相比,Java 寫起來確實是比較麻煩一些,你必須明確的宣告各個變數的型別,有些時候甚至需要重覆撰寫這些型別資宣告。 例如說在開發 Java 程式的時候,我們經常會使用到 HashMap 這個類別來存放 Key-Value 這樣型式的配對,若在 Java 當中的話,我們必須這樣撰寫: HashMap<Int, String> userIDToNickname = new HashMap<Int, String>(); userIDToNickname(1, "nickname1"); userIDToNickname(2, "nickname2"); 相較之下,若使用 Python 來撰寫類似的程式,則只需要把注意力放在資料的設定上即可: userIDToName = {1: 'nickname1', 2: 'nickname2'} 讀者可能會很很好奇,如果說 Scala 是靜態型別的編譯式程式語言,那麼會不會也會和 Java 有相同的問題,是不是也需要在寫程式的時候重覆輸入許多相同的型別資訊呢? 確實,Scala 在許多時候仍然需要使用者提供型別相關的資訊,但另一方面由於引入了型別推論的功能,很多時候 Scala 能夠猜測出它所需要的型別是什麼,以上述的 HashMap 為例,在 Scala 中只需要使用如下的程式碼即可達成: val userIDToName = Map(1 -> "nickname", 2 -> "nickname") 在這個例子中,Scala 的編譯器可以自動推論出 userIDToName 實際上是一個 Map[Int, String] 的變數,而使用者不需要重覆告知編譯器這個已知的型別資訊。 靜態型別也可以當 Script 用另一個對於靜態型別的編譯式程語言的常見批評,就是在撰寫程式的時候必須先有一個完整的骨架(如上述 Java 程式中的 但事實上 Scala 也可以使用類似 script 的模式運行,舉例而言,若要以 Scala 撰寫一個 Hello World 的程式,只需要下面這一行程式碼即可: println("Hello World") 若把上述的內容存成 $ scala HelloWorldScript.scala Hello World 雖然這樣的行為看起來很像動態型別的 Script 程式語言,但由於 Scala 是靜態型別的,所以會在執行前先檢查過程式碼,確認所有的型別都正確無誤後,這才會執行。這和 Ruby / Python 等程式語言的行為有一些不同。 下面的兩組程式碼可以說明這個行為上的差異,我們在下面的 Python 程式碼裡,故意傳入一個字串而不是 float 型別的資料給 import math print("Hello World") x = math.sqrt("string") print("End World") 由於 Python 是直譯式的程式語言,所以會先執行能夠執行的部份,一直到發現型別錯誤,這才發出執行期的 Exception 並退出程式。在執行的結果當中,我們可以看到第二行 Hello World 被印出,直到執行到第二行後程式才被強迫退出,並告訴使用者 Hello World Traceback (most recent call last): File "test.py", line 3, in <module> x = math.sqrt("4") TypeError: a float is required 另一方面,若我們在下面的 Scala 程式碼中,同樣傳入一個字串給一個要求參數是 Double 資料型態的函式,又會發生什麼事呢? println("Hello World") val x = Math.sqrt("string") println("End World") 在這個情況下,僅管是運行在 script 模式下,但由於這隻程式中有型別錯誤,因此完全不會被執行,只會由 Scala 告知使用者有型別錯誤存在: /home/brianhsu/test.scala:2: error: type mismatch; found : String("string") required: Double val x = Math.sqrt("string") ^ one error found 換句話說,任何 Scala 程式在執行前,都會確保程式碼中的型別是符合該函式要求的,以及函式或變數名稱真的存在,才會被執行。 編譯式語言也可以有 REPLPython / Ruby 等程式語言另一個令人讚賞之處,在於他們通常提供了稱為 REPL (read-eval-print-loop) 的工具,在這個工具中使用者可以輸入各種運算式,並且觀察運算的結果,或者嚐試呼叫某些函式,待確定該運算式正確無誤後,這才寫入程式碼中。 雖然說 Scala 是靜態型別的編譯式語言,但也提供了同樣的工具。在安裝完 Scala 編譯器後,若直接執行不帶任何參數的 舉例來說,在 REPL 中可以直接輸入運算式進行四則運算,並且 Scala REPL 會返回該運算式的計算結果。 $ scala Welcome to Scala version 2.10.3 (OpenJDK 64-Bit Server VM, Java 1.7.0_45). Type in expressions to have them evaluated. Type :help for more information. scala> 10 + 30 * 4 res0: Int = 130 scala> 不過 Scala REPL 更實用的一個功能,是他可以用來測試你不熟悉的函式庫,讓使用者不需要撰寫完整的程式碼就可以進行試驗。舉例而言,若我們不確定 Java 的 String 類別中的 substring 函式裡第二個參數所指定的位置,是否會被包含在返回的字串中,但又無法立刻存取到 Java 的 API 說明文件,那麼我們可以使用 Scala 的 REPL 進行測試與實驗: scala> "HelloWorld".substring(1, 3) res1: String = el scala> "HelloWorld".substring(0, 3) res2: String = Hel 而從這樣的實驗中,即便我們手上沒有 Java 的 API 文件,也可以推測出 substring 的第一個參數指定的位置的字元會被包含在返回的字串中,而第二個參數指定的位置則不會被包含。 小結透過上述的介紹,我們可以發現雖然 Scala 是靜態型別的編譯式程式語言,但由於提供了型別推論以及其他的工具,因此可以解決許多對於靜態型別與編譯式程式語言的缺點,讓撰寫 Scala 程式和撰寫其他動態型別的直譯式語言一樣方便,也可以先從最小的運算式開始實驗,確定沒問題之後,這才往上建構完整的程式。 從下一篇開始,我們將會開始準備 Scala 的開發環境,並且正式進入 Scala 程式語言的教程。 |