選擇的自由
繼讀來說說 Scala 為什麼吸引我唄,在這一篇裡面與其他語言比較的部份沒那麼多,大部份是在講 Scala 和 Java 的一些不同,以及舉例說明我喜歡 Scala 的另一理由--它給你選擇的自由,並且相信你會正確使用手上的工具。
變數有兩種
在純 Functional Programming (FP) 的語言中,嚴格的限定了變數在執行時期是不可變動的,舉例來說,如果你在 Haskell 用了如下的宣告,那麼 x 永遠就是 5,你不能再將他重新指定成 10。
let x = 5 -- x 初始化為 5 x = 10 -- 這是不允許滴--parse error on input `='
而相較之下,在 C / C++ / Java 這類程序導向或物件導向的程式語言中,我們非常習慣經常性地修改變數的內容。
public class Test { public static void main (String [] args) { int sum = 0; for (int i = 0; i <= 10; i++) { sum = sum + i; System.out.println("i:" + i); // i 一直變 System.out.println("sum:" + sum) // sum 也一直變 } } }
至於 Scala 呢?Scala 兩個都給你,一個叫做 val,是不可以重新指定的變數,一個叫做 var,是可以重新指定值的變數,但是在眾多的教學的文件裡面,都會告訴你偏好使用 val。事實上,寫出來後也常常會發現,一隻程式中根本完全沒出現過 var 變數。
舉例來說,Scala 允許你寫出和上面 Java 版相似的程式碼:
var sum = 0; // 我是可以被更動滴! for (i <- 0 to 10) { sum = sum + i; println ("i: " + i) // i 一直變 println ("sum:" + sum) // sum 也一直變 }
但是如果你實際上去翻大家寫的 Scala 程式碼,幾乎沒有人會用上面的寫法,而是偏好採用 Function Programming 的方式來寫:
val xs = (1 to 10).toList // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val sum1 = xs.sum // Scala 2.8 起提供的作弊函式 val sum2 = xs.reduceLeft(_ + _) // 標準的 Function Programming 做法 // sum1 = 20 // 編譯錯誤 // sum2 = 30 // 編譯錯誤 /** * 到底 xs.reduceLeft(_+_) 是啥呢? * 展開後會變這樣: * * Step 1: * 先將第一個元素和第二個元素放到 (_ + _) 的底線部份,成為 (1+2), * 再把 (1+2) 的結果取代掉第一二個元素,所以現在整個數列變成: * List(3, 3, 4, 5, 6, 7, 8, 9, 10) * * Step 2: * 再將上面的 List(3, 3, 4, 5, 6, 7, 8, 9, 10) 繼續做同樣的動作,變成: * List(6, 4, 5, 6, 7, 8, 9, 10) * * Step 3, 4, 5, 6, 7, 8, 9: * List (10, 5, 6, 7, 8, 9, 10) * List (15, 6, 7, 8, 9, 19) * List (21, 7, 8, 9, 10) * List (28, 8, 9, 10) * List (36, 9, 10) * List (45, 10) * List (55) * * Step 10: 將 55 取出,所以 sum2 = 55 * */
這大概就是 Scala 對於許多事情的中心思想--許多的功能與典範都有自己擅長的地方,我們不應該強迫寫程式的人用什麼方式思考,而是儘可能地給你合適的工具,並且信任你不會在需要螺絲起子的時候去拿刀片,而也不會限制你只能用螺絲起子卻不能用刀片。
我必須很誠實的講,我喜歡這樣想法,這或許也是我一直很難真正認真去學習 Haskell 之類的 Pure Functional 程式語言的原因之一吧--我比較喜歡在我覺得刀片比較順手的時候去用刀片,而不是被迫只能用螺絲起子來完成所有的事情。
偏好 Immutable 物件,但也給你選擇的自由
同樣的,由於 Scala 將 FP 做為預設偏好的編程典範,所以也提倡所謂的 immutable 物件,至於什麼是 immutable 物件呢?簡單地來說,就是不論你對他做什麼樣的操作,他都永遠不會改變。
其中一個著名的例子是 Java 當中的 String 物件,不論你呼叫 String 裡的哪一個函式,原來的 String 物件是不會被更動的,只是產生了一份新的 String 物件而已。
例如:
public class Test { public static void main (String [] args) { String str1 = "Hello World"; String str2 = str1.substring(0, 5); System.out.println (str1); // "Hello World" System.out.println (str2); // "World" } }
同樣的,在 Scala 當中,幾乎大部份預設的資料結構都是 immutable 的,特別明顯的就是 Collection 的相關類別,像是 List、Map 這些東西,如果沒有特別指明的話,所使用的都是 immutable 的版本。
例如:
val xs1 = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val xs2 = xs1.filter(_ % 2 == 0) println (xs1) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) println (xs2) // List(2, 4, 6, 8, 10)
至於 immutable 物件的好處,諸如 Thread-safe 等等,其他地方都已經把他講到爛了,所以這邊就不再贅述。
不過要特別提的一點,就是 Scala 給你選擇的自由,他把一整套工具都給你了,所以當你發現真的需要 mutable 物件的 Collection 時候,也沒有問題,只要選用 scala.collection.mutable 裡的東西就可以了,幾乎所有的容器都有一份 immutable 和 mutable 版本,所以你可以選擇適合自己需求的東西。
在 Scala 中的 == 運算子
Java 當中的 == 運算子在不同的狀況下有不同的語意:
- 當比較的變數是基本資料型態(如 int 或 boolean 這些),比的是『值』
- 當比較的變數是 Reference,比的是該 Reference 是不是指到同一個物件
一個常常用來示範 Java 中 == 運算子的範例程式如下:
public class Test { public static void main (String [] args) { int x1 = 123; int x2 = 100 + 20 + 3; String strPool1 = "Hello World"; String strPool2 = "Hello World"; String str1 = new String("Hello World"); String str2 = new String("Hello World"); // true,因為值相等,都是 123 System.out.println (x1 == x2); // true,因為指到 String Pool 裡的同一個物件 System.out.println (strPool1 == strPool2); // false,雖然值都是 "Hello World",但是是不同物件 System.out.println (str1 == str2); } }
相較之下,由於 Scala 是個純物件導向語言,已經沒有基本資料型態和 Reference 資料型態的概念,所以對於 == 的處理也有一點不同--在 Scala 當中,== 用以比較值(在物件有正確實作 hashCode 與 equals 時)。
簡單的來說,在 Scala 當中的 x == y 約相當於在 Java 中呼叫 x.equals(y) 這樣子,這是和 Java 差別滿多的一點。也因為如此,如果真的需要比較兩個變數是不是指到同一份物件,Scala 另外提供了一個叫做 eq 的運算子。
範例如下:
val x1 = 123 val x2 = 100 + 20 + 3 val strPool1 = "Hello World" val strPool2 = "Hello World" val str1 = new String("Hello World") val str2 = new String("Hello World") val xs1 = List(1, 2, 3) val xs2 = List(1, 2) + List(3) println (x1 == x2) // true println (strPool1 == strPool2) // true, 內容都是 "Hello World" println (strPool1 eq strPool2) // true, 同一個物件 println (str1 == str2) // true, 內容都是 "Hello World" println (str1 eq str2) // false, 不同的物件 println (xs1 == xs2) // true, 內容都是 List(1, 2, 3) println (xs1 eq xs2) // false, 不同的物件
但相較之下,如果你沒有自己實作 equals 這個 method 的話,預設會去比較兩個變數是不是指到相同的物件,算是比較要注意的地方。
像是如果我們設計了一個 Complex 類別來表示數學上的複數(一個實數 r 加上一個虛數 i),但沒有實作 equals 方法,就會發生像下面的慘況(?)。
class Complex(val r: Double, val i: Double) val c1 = new Complex(3, 4) val c2 = new Complex(3, 4) println (c1 == c2) // false,因為沒有實作 equals println (c1 eq c2) // false,因為是不同的物件
Operator?不,是 Method!
剛剛看到了 == 這個運算子的運作方式,但其實這邊有個問題--== 這個符號並不是運算子,而是 Method!如果去翻 Scala 的標準函式庫的 ScalaDoc 文件,就會發現 == 這個東西被定義在 Any 和 AnyRef 這兩個類別上,而且是 Method(用 def 定義)!
在對於運算子的處理上,Scala 和 Haskell 的做法比較接近--所謂的運算子只是一種語法糖衣,本質上是 method (in Scala) 或是 function (in Haskell)。
也就是說,在 Scala 中你看到的 + 其實不是 + 這個運算子,而是一個叫做 + 的 method,你看到的 - 不是 -,而是一個叫做 - 的運算子。而唯一的規則是,如果你的 method 只接收一個參數,那你可以將 . 和括號去掉。所在在 Scala 中,1 + 2 其實是等價於 1.+(2) 的,只是 . 和 + 號可以被省略罷了。
舉例來說,我們知道在數學上如果一個複數 a+bi 加上另一個複數 c+di 會等於 (a+c)+(b+d)i,如此一來,我們可以擴充上面我們剛剛寫出來的 Complex 類別,幫他加上一個 add 的 method,來做複數的加法。
class Complex(val r: Double, val i: Double) { def add (other: Complex) = new Complex(r+other.r, i + other.i) override def toString() = "Complex(%.2f, %.2f)" format(r, i) } val c1 = new Complex(4, 3) val c2 = new Complex(2, 4) val c3 = c1.add(c2) // Complex(6.00, 7.00) val c4 = c1 add c2 // Complex(6.00, 7.00)
看見了嗎?c1.add(c2),因為在這邊 add 只接受了一個參數,所以可以改寫為 infix operator 的型式,變成 c1 add c2 這個樣子。
但是等等,如果都已經寫成了 c1 add c2,那為什麼不乾脆使用數學上習慣使用的 + 號來當做 add 這個 method 的名稱呢?沒問題,Scala 允許你這麼做!
我們只要將上面的 add 改成 + 號,你就多出了一個 + 號運算子。
class Complex(val r: Double, val i: Double) { def + (other: Complex) = new Complex(r+other.r, i + other.i) override def toString() = "Complex(%.2f, %.2f)" format(r, i) } val c1 = new Complex(4, 3) val c2 = new Complex(2, 4) val c3 = c1 + c2 // Complex(6.00, 7.00)
至於 Operator 中最重要的優先權(例如先做 * / 再做 + -)和左結合右結合這些概念,Scala 則是依照你所使用的 method 名稱來定義(像是 * 這個名字的 method 比 + 這個名稱的 method 優先權高,以冒號結尾的 method 是右結合),詳細的說明請參照 Scala Reference - 6.12.3 Infix Operator 這份文件。
其實 Operator Overloading 一直以來都具有諍議性的話題,甚至已經有點像是 Python 該不該用縮排做 Token 之類的諍議了。
但在這方面,我和 James Iry 的看法比較接近,就算把 Java 把 Operator Overloading 拿掉了,還是會看到一大堆名稱與內容不符的函數名稱(淦,當你看到有函數名稱叫做 checkPathExists 但是卻是去做 mkdir 的時候你就會想哭了)。
只因為怕被人誤用,就把 Operator Overloading 拿掉,是有點因噎廢食了。而且 Scala 在這邊的處理我也覺得還好,至少處理上都還滿一致和自由的,不像 C++ 只能選擇固定的幾個符號來做 Operator。
畢竟在 Scala 中就算不使用符號,還是可以使用 infix notation,所以沒必要特定把 method 名稱取成奇怪的符號,例如我覺得 myList filter someCondition 這樣的寫法就很方便和清楚了。
此外,就算有人發現你在 Scala 裡面用了 Operator Overloading 而開始對你說教的時候,你也可以大聲的說:『我沒有用 Operator,我只是呼叫了 Method 而已啊!』你看看,Scala 真是支 Operator Overloading 的人的好朋友啊,連退路都幫你想好了!(大誤)
(謎之音:你這個根本是在狡辯吧!=_=)
回響