Scala Tutorial(1)靜態型別也可以很方便 by brianhsu | CodeData
top

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 檔

相較之下,要撰寫一個 HelloWorld.scala 並且執行的話,整個編譯與執行的流程會像下面一樣:

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 的編譯器除了產生的是兩個 .class 檔案而不是只有一個以外,其產出和 javac 編譯器並沒什麼不同。

事實上,由於 Scala 所編譯出的 .class 檔是標準的 Java Bytecode,所以只要我們提供了 Scala 標準函式庫的 JAR 檔給 java 指令的話,是可以直接使用 java 指令執行 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 程式中的 publc classpublic static void main 等等,以及需要先經過編譯的部驟才能執行。

但事實上 Scala 也可以使用類似 script 的模式運行,舉例而言,若要以 Scala 撰寫一個 Hello World 的程式,只需要下面這一行程式碼即可:

println("Hello World")

若把上述的內容存成 HelloWorlScript.scala 這個檔案的話,我們可以直接使用 scala 指令將其做為 script 執行,並且會正確地在螢幕上印出 Hello World 字樣。

$ scala HelloWorldScript.scala 
Hello World

雖然這樣的行為看起來很像動態型別的 Script 程式語言,但由於 Scala 是靜態型別的,所以會在執行前先檢查過程式碼,確認所有的型別都正確無誤後,這才會執行。這和 Ruby / Python 等程式語言的行為有一些不同。

下面的兩組程式碼可以說明這個行為上的差異,我們在下面的 Python 程式碼裡,故意傳入一個字串而不是 float 型別的資料給 sqrt 函式:

import math
print("Hello World")
x = math.sqrt("string")
print("End World")

由於 Python 是直譯式的程式語言,所以會先執行能夠執行的部份,一直到發現型別錯誤,這才發出執行期的 Exception 並退出程式。在執行的結果當中,我們可以看到第二行 Hello World 被印出,直到執行到第二行後程式才被強迫退出,並告訴使用者 sqrt 要求的是 float 型別的資料。

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 程式在執行前,都會確保程式碼中的型別是符合該函式要求的,以及函式或變數名稱真的存在,才會被執行。

編譯式語言也可以有 REPL

Python / Ruby 等程式語言另一個令人讚賞之處,在於他們通常提供了稱為 REPL (read-eval-print-loop) 的工具,在這個工具中使用者可以輸入各種運算式,並且觀察運算的結果,或者嚐試呼叫某些函式,待確定該運算式正確無誤後,這才寫入程式碼中。

雖然說 Scala 是靜態型別的編譯式語言,但也提供了同樣的工具。在安裝完 Scala 編譯器後,若直接執行不帶任何參數的 scala 指令,就會進入 Scala 的 REPL 環境中,我們可以在這個環境裡實驗並觀察各個函式的效果。

舉例來說,在 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 程式語言的教程。

後續 >> Scala Tutorial(2)準備開發環境、Scala 中的四則運算

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章