[Scala] 我為何鍾情於用 Scala 做為自己的兵刃(二)--靜態型別的動態語言。

就如同武林人士一樣,要找到一把適合自己的兵刃,往往不是一天兩天的事情,我在找到 Scala 之前,也經常去看各種的程式語言的教學之類的,總希望找到自己中意的程式語言。

在這一系列中,經常會將 Scala 與其他語言做比較,用以說明我為何喜歡上了 Scala ,但其實我並沒有真正在使用這些用來舉例的語言,所以如果其中有什麼謬誤,還請見諒。

另一方面,我也說過了,我覺得程式語言沒有什麼好壞之分,在這邊的舉例只是單純用來說明我喜歡 Scala 的緣由,如果批評到你喜歡的程式語言,同樣也請見諒,我想這只是大家的偏好不同罷了。

首先,我愛上 Scala 的一點,就在於他有著『靜態型別的動態程式語言』這個稱號。

這裡舉一個簡單的例子,我們將會用 Java / Ruby / Python / Scala 四種不同的程式語言來實作,並觀察 Scala 到底有什麼令我愛不釋手的地方。

下面是程式的規格:

- 宣告一個名為 zipCode 的對應表,這個對應表是郵遞區號(整數)到地區(字串)的對應
- 宣告一個 getArea221(shouldCrash) 的函式,這個函式會取得 221 這個郵遞區號的地區
    - 如果 shouldCrash 為 false,用整數 221 來當做索引取出 zipCode 相對應的值
    - 如果 shouldCrash 為 true,用字串 "abc" 當做索引取出 zipCode 相對應的值
    - 當然,既然是郵遞區號,用字串 "abc" 來查詢是完全不合理的
- 在主程式中執行下列下三個步驟
    - 印出 Hello World
    - 呼叫並印出 getArea221(false) 的值
    - 呼叫並印出 getArea221(true) 的值

首先來看一下 Java 版的實作:

// This is Java.
import java.util.HashMap;

public class Test
{
    static HashMap<Integer, String> zipCode= new HashMap<Integer, String>();

    static String getArea221 (boolean shouldCrash)
    {
        if (shouldCrash) {
            return zipCode.get("abc");
        } else {
            return zipCode.get(221);
        }
    }

    public static void main (String [] args)
    {
        zipCode.put(221, "汐止");
        zipCode.put(115, "南港");
        zipCode.put(545, "埔里");

        System.out.println("Hello World");
        System.out.println(getArea221(false));
        System.out.println(getArea221(true));
    }
}

嗯,看起來就是相當的,嗯……Java,一堆囉哩八唆的規舉和型別,看了就討人厭!

試著編譯一下,可以過關,執行一下,產生了下列的輸出:

brianhsu@NBGentoo ~/test $ java Test
Hello World
汐止
null
brianhsu@NBGentoo ~/test $ 

等等!最後一個 null 是怎麼回事咧?我以為 Java 是靜態型別加上強型別的程式語言,應該可以在編譯時期就告訴我,我的程式有型別不符的地方不是嗎?又臭又長卻又沒什麼用,難怪大家討厭 Java,都跑去擁抱 Ruby 和 Python 了。(淚)

好,那我們來看一下最近的後起之秀 Ruby 的表現如何唄!

#!/usr/bin/ruby

@zipCode = {221 => "汐止", 115 => "南港", 545 => "埔里"}

def getArea221(shouldCrash)

    if (shouldCrash) 
        @zipCode["abc"]
    else
        @zipCode[221]
    end
end

puts("Hello World")
puts(getArea221(false))
puts(getArea221(true))

嗯,果然不負眾望,整整減少了近十行的程式碼,在簡潔方面,徹徹底底地打敗了 Java 啊!

那在抓出錯誤方面比之 Java 又如何呢?我們來執行這隻程式看看:

brianhsu@NBGentoo ~/test $ ruby test.rb 
Hello World
汐止
nil
brianhsu@NBGentoo ~/test $ 

喔喔!出現 nil 了!聽說 nil 在 Ruby 裡的地位大概就和 Java 中的 null 一樣,所以看來在這個例子裡,Ruby 在簡潔方面的表現勝出,但抓錯誤方面好像半斤八兩。

那麼這個月衝上 TIOBE Index 第五名,大家都說相當簡潔易學的 Python 的表現又如何呢,咱們來看看:

#!/usr/bin/python
#coding=utf-8

zipCode = {221: "汐止", 115: "南港", 545: "埔里"}

def getArea221(shouldCrash):

    if shouldCrash:
        return zipCode["abc"]
    else:
        return zipCode[221]

print ("Hello World")
print (getArea221(False))
print (getArea221(True))

大家的說法不是沒有道理的,基本上看起來和 Ruby 長得差不多,都很簡潔,所以在簡潔性方面也是大勝 Java。

不過不知道 Python 是不是能抓出這個程式碼不合理的地方呢?來試試看好了:

Hello World
汐止
Traceback (most recent call last):
  File "test.py", line 16, in 
    print (getArea221(True))
  File "test.py", line 9, in getArea221
    return zipCode["abc"]
KeyError: 'abc'

太棒了,Python 告訴我們有 KeyError 發生,也就是說字串 abc 不能當做 zipCode 的索引。

但是等一下,為什麼前面還是有 Hello World 和『汐止』被印了出來呢?嗯……因為這是『執行期錯誤』,也就是說,如果我們把程式改成下面這樣:

#!/usr/bin/python
#coding=utf-8

zipCode = {221: "汐止", 115: "南港", 545: "埔里"}

def getArea221(shouldCrash):

    if shouldCrash:
        return zipCode["abc"]
    else:
        return zipCode[221]

print ("Hello World")
print (getArea221(False))

很好,什麼事都不會發生,雖然這份程式碼實際上是有問題的,只要有人呼叫了 getArea221(True) 他就會在執行的時候毫不留情的爆炸給你看。

不過,會爆炸給你看,似乎總比給你一個 null 或 nil 好得多的樣子?!

最後,我們來看看我最愛的 Scala 長得怎樣:

// This is Scala.
val zipCode = Map(221 -> "汐止", 115 -> "南港", 545 -> "埔里")

def getArea221(shouldCrash: Boolean) = {

    if (shouldCrash) {
        zipCode("abc")
    } else {
        zipCode(221)
    }
}

println ("Hello World")
println (getArea221(false))
println (getArea221(true))

『呃……十五行?!你真的確定這是靜態型別的程式語言嗎?!』

沒錯,看起來很不可思議吧?!和 Ruby / Python 版的看起來差不多,唯一看到的型別好像也只有 Boolean 而已咧?!

如果不相信,我們來執行一下:

brianhsu@NBGentoo ~/test $ scala test.scala 
/home/brianhsu/test/test.scala:7: error: type mismatch;
 found   : java.lang.String("abc")
 required: Int
        zipCode("abc")
                ^
one error found
brianhsu@NBGentoo ~/test $ 

呃,test.scala 第七行有型別錯誤,找到的是 java.lang.String,但需求的是 Int,這一行程式碼是 zipCode("abc")!

注意!Hello World 沒有被印出來,也就是說,這隻程式在還沒執行的時候就被發現錯誤了!而不像 Python 是要等到執行到那一行的時候才爆炸給你看。

事實上,就算你把最後一行拿掉,變成下面這樣:

// This is Scala.
val zipCode = Map(221 -> "汐止", 115 -> "南港", 545 -> "埔里")

def getArea221(shouldCrash: Boolean) = {

    if (shouldCrash) {
        zipCode("abc")
    } else {
        zipCode(221)
    }
}

println ("Hello World")
println (getArea221(false))

他一樣會爆給你看,證明了 Scala 確實是靜態型別的程式語言--在執行前,會先進行型別檢查,確定型別都沒問題之後,才會真的開始執行。

brianhsu@NBGentoo ~/test $ scala test.scala 
/home/brianhsu/test/test.scala:7: error: type mismatch;
 found   : java.lang.String("abc")
 required: Int
        zipCode("abc")
                ^
one error found
brianhsu@NBGentoo ~/test $ 

但如果我把上面的程式碼中的 abc 改成整數 115,那程式就可以正常執行了,同時執行的結果也會一如預期的是分別印出 Hello World、汐止和南港這三行字,如下所示:

// This is Scala.
val zipCode = Map(221 -> "汐止", 115 -> "南港", 545 -> "埔里")

def getArea221(shouldCrash: Boolean) = {

    if (shouldCrash) {
        zipCode(115)
    } else {
        zipCode(221)
    }
}

println ("Hello World")
println (getArea221(false))
println (getArea221(true))
brianhsu@NBGentoo ~/test $ scala test.scala 
Hello World
汐止
南港
brianhsu@NBGentoo ~/test $ 

如何,是不是很神奇呢?這也是我喜歡上 Scala 的一個原因--他或許比 Ruby / Python 這些動態語言多了一些型別標示(主要是在函式宣告的部份),但看起來仍然比 Java 簡潔很多,但同時又是靜態型別與強型別的程式語言。

這對我來說是相當重要的一點,因為我很清楚我是個粗心的人,之前寫 PHP 時常常 debug 半天,結果卻發現只是因為我把變數或函數的名稱打錯,而 Scala 可以在維持程式碼簡短的同時,又幫我抓出類似的錯誤,怎麼能讓我不欣賞他呢?!

Update 2010/01/12 17:41:

下面有朋友提出來 Java 此舉是為了保留彈性,雖然我不清楚這邊所謂的彈性指的是什麼,不過這提醒了我,上面的例子其實沒有完全表現出 Scala 的有趣之處,畢竟 zipCode 就是一個整數對應到字串的 Hash Table 嘛!而愛用 Ruby / Python 的朋友可能也會想到,Ruby / Python 允許你在同一個 Hash Table 中用不同型態的東西當 Key。

那麼,Scala 又是如何呢?事實上,讓我對 Scala 感到驚豔的地方,就在於他的行為通常都合理到讓你覺得不可思議,常常就是你要的東西。

例如,下列的 Scala 程式碼是合法的:

val map = Map(1 -> "一", 2 -> "二", 2.1 -> "二點一")

println (map(1))
println(map(2))
println(map(2.1))

執行結果:

brianhsu@NBGentoo ~ $ scala test.scala 
一
二
二點一
brianhsu@NBGentoo ~ $ 

看吧!Scala 也可以用不同的型態當做鍵值喲!那如果在這個範例中,試著用字串來取出 map 裡的東西又會如何呢?!

val map = Map(1 -> "一", 2 -> "二", 2.1 -> "二點一")
println (map(1))
println (map("我是字串"))

他會告訴你……

brianhsu@NBGentoo ~ $ scala test.scala 
/home/brianhsu/test.scala:3: error: type mismatch;
 found   : java.lang.String("我是字串")
 required: AnyVal
println (map("我是字串"))
             ^
one error found
brianhsu@NBGentoo ~ $ 

編譯時期的型態錯誤,如何,神奇吧?!這樣的行為真的是深得我心啊,叫我怎麼能不愛上他呢?!

回響