2011年5月18日 星期三

5-15. Uniform access principle

Scala 嘗試將 data field 與 method 做進一步的一致化,也就 data field 與 method 區分不要再這麼清楚,以後可以視為同一個種類(可以把這二者想像是同一個東西)。一致化的好處是,sub class 可以很容易使用想要使用的方式,override parent 的 method 或是 val。

因為這個概念,衍生了以下這些我們之前討論的事情
1. data field 會產生對應的 method
2. method 與 data field 的 name space 是同一個
3. val 可以 override val
4. val 可以 override def

「Uniform access principle」就是希望 class 的使用者端,可以不理會 class 真正的宣告方式如何,可以使用相同的方式來 access 該 class 的資料。
舉例而言:有一個資料結構,有 length 的 property。length 可以是使用 data field 存起來,也可能是使用 method 來算出來,但 Scala 希望你都可以直接使用 obj.length 來 access 該 property。這就叫「uniform access principle」。

那要如何達到「uniform access principle」?

你可以使用我們之前所提到的「data member access」原則

我們要達到使用 obj.length 的方式,可以使用的方法
1. 使用 data member 宣告
class S {
  var length = 0
}
2. 使用 access methods
class S {
  def length = ...
  def length_=(n: Int) = {...}
}
請注意:access method 的 reader method 不可使用括號。

Uniform access principle 的概念將 data field 與 method 綁在一起,所以才會有一些混合 method 與 val data field 的用法,這是我們需要特別注意的地方。

5-14. Method override

如之前篇幅所提,Scala method 宣告與 Java 精神類似。但使用上有點不太一樣,我們一一說明

一. method override 需要清楚指明

Java 使用 @Override annotation 來指明要 override。使用 @Override 的好處是,你可以避免 override 了錯誤的  method,或是打錯字。建議你在使用 Java 時,需要善用 @Override。

Scala 承繼這個精神,並且發揚光大。在 Scala 中,若是你要 override method,需要特別指明,指明的方式,就是在 method 宣告時,使用 override 這個 keyword。
1. 若沒有指明 override,但你宣告一個與 parent class 相同的 method,compiler 會要求你確認是要 create 一個新的  method,還是要 override parent 的 method。
2. 若使用 override ,但祖先 class 並沒有宣告相同的 method,表示你的 override,是有問題的,compiler 會要求你修正正確。
範例:override
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  override def m1() = 20 // 明確說明 override m1
}
範例:override 錯誤的 method
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  override def m2() = 20 // 明確說明 override m2,但 S1 沒有 m2,所以 compile error
}

範例:沒有 override,但 method 與 parent class 相同
class S1 {
  def m1() = 10
}
class S2 extends S1 { // 繼承 S1
  def m1() = 20 // m1 在 parent class 出現過,所以此處會有 compile error
}

二. abstract method 可以不需宣告 override
前面有篇幅提過 abstract method,就是沒有 implement 的 method。
若你的 method 是 override abstract method,此時可以宣告 override,也可不宣告 override
範例:abstract method,可以不需宣告 override
abstract class S1 {
  def m1() //m1 是 abstract method,因為沒有 method body
}
class S2 extends S1 { // 繼承 S1  
  def m1() = 20 // m1 在 parent class 出現過,但因為是 abstract method,所以可以不宣告 override
}
範例:abstract method,也可以宣告 override
abstract class S1 {
  def m1() //m1 是 abstract method,因為沒有 method body
}
class S2 extends S1 { // 繼承 S1  
  override def m1() = 20 // m1 在 parent class 出現過,但因為是 abstract method,也可宣告 override
}

三. 「空括號 method」與「無括號 method」可以互相 override
雖然「空括號 method」與「無括號 method」,似乎不太一樣,但相同的地方仍然很多,所以 Scala 把他們視為一體,所以可以互相 override。

範例:「空括號 method」override「無括號 method」
class S1 {
  def m1 = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override def m1() = 20 // S2.m1() override S1.m
}

範例:「無括號 method」override「空括號 method」
class S1 {
  def m1() = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override def m1 = 20 // S2.m1 override S1.m()
}

在 Scala 中的 val field,與其說是一個 data field,更像是一個常數的 method,把它當成 method,有時反而更加適當。def 也可以像 method 一樣 override,甚至與 def 之間可以 override。

四. val 與 val 間就像 def 一樣,override 的規定要求相同
範例:val override val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  override val m1 = 20 // val override val
}
範例:同名的 val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  val m1 = 20 // 與 parent 同名的 val,需要宣告override
}

五. val 可以 override def
def 定義一個 method,val 定義一個常數。Scala 中強調不可變資料的重要性,所以 val 可以 override def,但反向是不允許的。
範例:val override def
class S1 {
  def m1() = 10 //m1 是 空括號 method
}
class S2 extends S1 { // 繼承 S1  
  override val m1 = 20 // val override def
}
class Test {
  def test {
    val s2 = new S2
    s2.m1 // 此時會得到 20
    s2.m1() // 糟糕,這裡會出錯,因為 S2 並沒有 m1()
    val s1: S1 = s2
    s1.m1() //可以使用了
  }
}
注意:上例第 11 行的 s2.m1() 是錯誤的,因為 S2 沒有宣告 m1(),只有 val m1,此 val m1 是 override S1.m1()。
但這很奇怪啊?S2 的 val m1 可以 override S1.m1(),但卻不能使用該 m1() method?
這是因為我們需要回歸到「uniform access principle」(下節會提到)的本意,是讓沒有括號的 method 與 val 混用,但此時 s2.m1() 是希望直接使用 method,這就不符合「uniform access principle」原先的想法,所以被禁止了。
你若是還是希望使用 m1() method 的方式,此時需要先將 s2 cast 成一個 S1 的 instance,這時就可以使用了,方式就如同上例的第 12, 13 行一樣。

範例:def override val
class S1 {
  val m1 = 10 //m1 是 val
}
class S2 extends S1 { // 繼承 S1  
  override def m1 = 20 // def override val,這是錯誤的
}

上例打算使用 method 來 override val,當然不會允許,原因是 val 被視為常數,所以一定是不能被 override 的。

5-13. 「無括號 method」與「空括號 method」

Scala 的 method 宣告,雖然語法與 Java 不同,但精神 Java 的類似。
class S {
  def m1() = {...}
}

但 Scala 的 method 在處理無參數的 method 時,與 Java 有滿大的不同。

在 Scala 中,若 method 沒有參數,method 宣告時可以不包含小括號。但需要注意的是,沒有小括號與有小括號的 method 二者是不一樣的。

無括號的 method 稱為「無括號 method」,而有括號但無參數的 method,稱為「空括號 method」。

呼叫時,無括號的 method 需使用無括號的呼叫方式。而有括號的 method,可以使用有括號的呼叫方式,但由於小括號省略原則,也可以使用無括號的呼叫方式。

範例:「空括號 method」
可以使用有小括號的呼叫方式,或是無小括號的呼叫方式
class S {
  def m() = {....}
}
class Test {
  def test() = {
    val s = new S
    s.m() //使用有小括號的呼叫方式
    s.m //小括號省略原則,所以這是合法的
  }
}

範例:「無括號 method」
只可使用無小括號的呼叫方式
class S {
  def m = { //這裡是沒有小括號的 method
    ...
  }
}
class Test {
  def test() = {
    val s = new S
    s.m // 這是正常的 method 呼叫,並非使用小括號省略原則
    s.m() //由於 m 其實是無括號之 method,所以這種呼叫方式是錯誤的
  }
}
上例很清楚看到,無括號 method 不能使用有括號的呼叫方式。

雖然「無括號 method」與「空括號 method」,兩者被視為兩個不同的 method,但同名的這兩種 method,仍不能同時存在。
範例:同名的「無括號 method」與「空括號 method」,不可一起存在。
class S {
  def m() = .... // 空括號的 method
  def m = ... // 無括號 method
  }
}
上例,有兩個 method m,會有 compile error。

「何時使用空括號,何時使用無括號?」

在 Scala 中,「無括號 method」很常見,通常用在於直接 return 回一個 data field 的值。所以,無括號 method 通常不會有冗長的 code 以及 side effect。
相反的,空括號的 method,則與一般的 method 一樣,該 method 可以處理很多事,或是有 side effect。其與其他 method 的差異點,只在於它剛好沒有參數而已。

無論是「無括號 method」或「空括號 method」,由於小括號原則,我們都可以使用無括號的方式來呼叫。

「那何時使用有括號呼叫,何時使用無括號呼叫?」

與上述決定「何時宣告無括號 method」的通則一樣。當呼叫到一個複雜的 method,Scala 建議你使用空括號的呼叫。若你是呼叫一個無 side effect 的 method,這時建議你使用無括號的呼叫方式。

總之,不論是宣告,還是呼叫,無括號的 method,使用在無 side effect 時。而空括號的 method,使用在複雜或有 side effect 的情況下。

5-12. 小括號省略原則

為支援快速開發的目標,在 Scala 中,不像 Java 那麼重視小括號。
在 Java 中,小括號需要存在的地方,我們不可隨意省略。
但在 Scala 中,我們有很多地方都可以省略括號,尤其是與 class 相關,有許多地方都可以省略小括號,。

小括號最常使用在 method 宣告與 method call 時,在我們進一步討論 method 時,我們先說明一下「小括號省略原則」,以下是可以省略的用法
  1. create instance 時,若 constructor 不需有參數,小括號可以省略。這已在上篇討過。
  2. 在 method 宣告時,若該 method 沒有參數,可以宣告無小括號的 method。
  3. 在 method 呼叫時,若是不需要有參數,可以省略點(dot) 與小括號。
  4. 在 method 呼叫時,若是只有 1 個參數,可以省略點(dot) 與小括號。
不要小看小括號省略原則,我們後來將會看到小括號省略將會如何影響 Scala 的語法,說小括號省略影響 Scala 的設計理念其實也不為過。我們將於以下章節繼續討論。

5-11. Data member 與 Access methods

關於 data member,Scala 編譯出來的 bytecode,遵循 OO 傳統的 encapsulation 觀念,會將所有 data member 使用 private modifier 隱藏起來,並產生 access method 作為真正要 access 這些 data member 的方式。

在 Scala 宣告 data_member,會產生以下的東西:
1. 產生一個 private data member,若是常數,則會加上 final modifier
2. 產生 access method
    若是常數,只會有一個 reader 的 access method
    若是變數,會有 reader 與 writer 的 access methods
    access method 的 visibility 將會與該變數的 visibility 有關。

讓我們依序說明 data member 常數與 data member 變數。

範例:宣告一個 data member 常數
class S {
  val val_s: String = "abc"
}
使用 javap 查看,得到


可以瞭解,在 Scala 中的一個 data_member 常數  val_s,會產生
1. 一個 Java 的 data member:final private val_s
2. 一個 Java 的 access method:public val_s()

說明:
由於是 data member 常數,所以會變成 final 的 Java data member
由於 Scala 中的 member,default 是 public,所以 access method 的 visibility 是 public

讓我們將 data member 換成 private,看會產生何種變化
範例:宣告一個 private 的 data member 常數
class S {
  private val val_s: String = "abc"
}



我們可以看到,與上一個例子相同,只是 method val_s() 變成 private,可見得 access method 的 visibility 與 Scala data member 的 visibilty 相同。

接下來我們看 data member 變數,會產生什麼?
class S {
  private var var_s: String = "abc"
}




在 Scala 中的一個 data_member 變數  var_s,會產生
1. 一個 Java 的 data member:private var_s
2. 一個 Java 的 reader access method:public var_s()
3. 一個 Java 的 writer access method:public var_s_$eq()

說明:
由於是 data member 變數,所以該 Java data member 沒有加上 final
由於 Scala 中的 member,default 是 public,所以 access methods 是 public

由上面的例子,很清楚我們發現兩個事實
1.Scala 的 data member 常數,會編譯成 final private data member,以及 reader methods。
2.Scala 的 data member 變數,會編譯成 private data member,以及 reader / writer 兩個 methods。

該 data member 的 reader method,就是與 data member 同名的 method。
該 data member 的 writer method,就是 data member 名稱,後面再加上「_$eq」的 method。

Scala 中,有以下的概念「data member 使用原則」
  1. Scala 中,只要有 reader method,就可以把它當成 read 的 data member 來處理。
    • 所以只要有一個 method 叫 x(),我們可以把它視為有一個可讀的 x data member。
  2. Scala 中,只要有 reader / writer method,就可以把它當成 write 的 data member 來處理。
    • 所以只要有兩個 method 叫 x 與 x_$eq(),我們可以把它視為有一個可寫的 x data member,所以我們可以直接使用 obj.x = ... 的格式。
    • 但是,若只有一個 writer method,則無法把它當成 data member 來使用
請注意:「name_$eq()」這種格式的 method 名稱,是在 JVM 層次所看到的,若你想要在 Scala 中產生這種格式的 method,你可以宣告一個 method 叫「name_=()」。

範例:有 reader method x(),當成 read 的 data member 來看待
class S {
  def x() = 100 // S 有 x() method,可以將它視為 read only 的 x
}
class Test {
  def m1() = {
    val s = new S
    val n = s.x // 將 x 視為 S 的 data member 來處理
  }
}
請注意,上例使用 s.x 是合法,雖然 S 裡面沒有一個 data member x。

其實,Scala 中更有一個關於 method call 的小括號省略原則。小括號省略原則讓你可以省略小括號,所以可以使用 s.x,這裡的 「data member 使用原則」也讓你可以使用 s.x,兩者是相通的。

範例:有 reader method x 與 writer method x_$eq(),當成 write 的 data member 來看待
class S {
  var x1 = 0
  def x = x1
  def x_=(n: Int) = x1 = n // S 有 x_=() method,可以將它視為 write 的 x data member
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 將 x 視為 S 的 data member 來處理
    println(s.x) // 將 x 視為 S 的 data member 來處理,這時候將會印出 10
}
請注意:需要有 x 與 x_$ 兩個 method 才會被視為 data field
注意:x_$=() method 會編譯成 x_$eq() method。

上例中,「s.x = 10」可以看成是 syntax sugar,會被 compile 改成呼叫「s.x_(10)」
其實,我們應該更精確的講,Scala 中沒有所謂的 data field 的 assignment,所有的 data field assignment,都會被改變成 call obj.name_=(value)。

範例:有 writer method x_$eq(),但沒有 reader method x,此時不可當成 data member 來看待
class S {
  var x1 = 0
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡會出錯,有 writer 但 沒有 reader 仍不行
}
範例:有 writer method x_$eq(),有 reader method x(),此時不可當成 data member 來看待,因為 reader method 需為 method x
class S {
  var x1 = 0
  def x() = x1
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡會出錯,有 writer x_=() 但 沒有 reader x 仍不行
}

請注意,空括號的 method x() 與無括號的 method x,在 Scala 中是視為不同的,若想要當成 data assignment 的 method,所需要的是無括號的 method x()。

將上例改為如下例
class S {
  var x1 = 0
  def x = x1 // 無括號的 method
  def x_=(n: Int) = x1 = n // S 有 x_=() method,但沒有 def x method
}
class Test {
  def m1() = {
    val s = new S
    s.x = 10 // 這裡就會成功了
}

這裡我們需要重複之前的一句話,我們應該更精確的講,Scala 中沒有所謂的 data field 的 assignment,所有的 data field assignment,都會被改變成 call obj.name_=(value),但前提是 name 需要有兩個 method 一個是 name_=(value) method ,另一個就是 name 這個無括號的 method。

另外,當我們使用 obj.name 來 access 該 obj 的 name data field 時,其實會被轉變成呼叫 obj.name 這個 method 或是 obj.name() 這個 method (這兩個 method 沒辦法不會同時存在)。

綜合上述兩點,我們可以看出 Scala 對於 data field 的操作其實與 Java 完全不同,Java 會直接操作到該 data field,但 Scala 會將所有的 data field 都轉換成 method call,所以外部程式其實是無法直接碰觸到該 data field 的,這樣保留了一點點安全性。
Scala 沒有像一些語言為保留這樣的安全性而直接取消掉 data field 的操作方式,保留這樣的 data field 的使用方式給 programmer 帶來相當大的方便性,Scala 將 data field 的操作直接在語言直接轉會成 method call,我們可以看成這是 syntax sugar,但需要時時提醒自己,其實這只是呼叫到相對應的 method。

5-10. Data member 與 method 的 name space

在之前我們談過,Scala 的 class 宣告格式為:
class S {
  var var_n: Int = 10
  val val_s: String = "hello"
  def m(n: Int) = n + 10
}
上例中,我們宣告
1. 使用 var 宣告一個 data member 變數:var_n
2. 使用 val 宣告一個 data member 常數:val_s
3. 使用 def 宣告一個 method:m

在 Java 中,data member 與 method 的 name space 是不同的,所以 data member 與 method 可以同名。

但 Scala 中,data member 與 method 共用 name space,因此 Scala 中你不可以宣告 data member 與 method 同名。
其實,說清楚一點是,Scala 會為 data member 產生 access method。而所產生的 access method 會與 data member 同名。所以此時若再宣告同名的 method,將會造成兩個同名的 method,產生 compile 錯誤。

範例:Java 中,method 可以與 data member 同名。
class J {
  int n1 = 10;
  int n1() {
    return 10;
  }
}

範例:Scala 中,method 不可以與 data member 同名。
class S {
  var n1 = 10 // compiler 會編譯本 statement 成為一個 bytecode 的 data member,以及至少一個 access method n1()
  def n1() = 10 // compiler 會認為此 statement 重複宣告 n1()
}

2011年5月7日 星期六

5-9. Access control

OO 語言有一個重要的特色,就是資料封裝(data encapsulation),所謂資料封裝,就是希望外面使用這個 class 的呼叫者,不需知道該 class 是如何 implementation,只需知道怎麼應用即可。

資料封裝的達成方式,通常就是透過 access control來達成。
所謂「access control」就是在定義 class 時,你希望該 class 有多少資訊可以給外界使用。
access control 有時又會稱呼為「visibility」。

Java 的 access control 有 4 種
1. private:只有自己可以用
2. package:只有自己與同 package 的可以用,這是 default
3. protected:只有自己、同 package、以及 subclass 可以用
4. public:全世界都可用

Java 使用 modifier 來定義 method 與 data field 的 access control。Java access modifier 有 public、protected、private。並沒有 package 這種 modifier,但 default 是 package access。
Java 的 access control 相信大家都熟,所以不仔細介紹。

Scala 的 access control,大部分的概念與 Java 相同,也同樣使用 access modifier 來定義 access control,但有些地方不一樣。以下依序說明:

1. Scala 標榜快速開發,所以 default 是 public。只有那些你想要保護的 method 或 data field,你才需要使用 access modifier 加以保護。

2. Scala 的 access modifier 只有 private 與 protected,沒有 public。也沒有所謂的「package access」這種模式,因為沒有標明 access modifier,就表示是 public。但並不表示 Scala 無法做到「只讓相同 package 的 class 來 access」,Scala 使用「protection scope」來達到,protection scope 將詳述於下。

3.Private member:outer class 不能 access inner class 的 private field。
Java 把 inner class 當成與自己是同一個體,所以 inner class 與 outer class 彼此可以互相 access 對方的 private field。
舉例說明
class J {
  private int n_outer_private; //宣告一個 private data field
  class Inner {
    private int n_inner_private;
    void p() {
      int n = n_outer_private; //可以使用 outer private field
    }
  }
  void p_outer() {
    Inner inner = new Inner();
    int n = inner.n_inner_private; //可以使用 inner private field
  }
}
但 Scala 對待 private 的方式,比 Java 更加嚴謹。Scala 為支援 functional 與 concurrency 的因素,決定將 改變 Java 那種把 outer class 與 inner class 視為同一個體的觀念。

在 Scala 中,inner class 視為 implement 的手段,所以 inner class 會被保護起來。outer class 無法 access inner class 的 private field。但 inner class 仍然視為 outer class 的一員,仍舊可以 access outer 的 private field。
範例:outer class 不可 access inner class 的 private。但 inner class 可以 access outer class 的 private。
class S {
  private var n_outer_private = 10
  class Inner {
    private var n_inner_private = 10
    def inner_print = println(n_outer_private) // 這裡是 inner class 去 access outer class 的 private,這理是正確的
  }
  def outer_print = {
    val inner = new Inner
    val n = inner.n_inner_private // 這裡是 outer class 去 access inner 的 private,這裡會有 compile 錯誤
  }
}

4.Protected field 只有自己與 subclass 可以 access,移除 package 可以 access 的觀念。
在 Java,protected field 可以讓相同 package 的其他 class 來 access。但這個許可,在 Scala 被移除,所以 Scala 的 protected 只有自己與 subclass 可以 access。
同樣的,若希望 package 的其他 class 可以 access 被保護起來的 field,可以使用「protection scope」的方式來做到。

範例
class S1 {
  protected def p_protected() = println("S1")
}
class S11 extends S1 {
  def p_S11 = p_protected() // 使用 super class 的 protected,這是合法的
}
class S2 {
  val s1 = new S1
  def p_S2 = s1.p_protected() // 使用同一 package 的 protected,在 Scala 這是不合法的。但在 Java 這是合法的
}

5. Protection Scope:private 或是 protected 保護的 field,可以進一步訂定保護的範圍。
所謂保護的範圍,指的是我們可以擴充或縮小可以 access 的範圍。
比如 private 的 access 範圍,原來指的是自己的 class,但我們可以使用「protection scope」加以擴充或縮小。

格式為:
private[X] field 或
protected[X] field

private[X] field 代表設定該 field 為 private,但可以放大或縮小到 X 的範圍。
protected[X] field 代表設定該 field 為 protected,但可以放大或縮小到 X 的範圍。

這個[X]稱為 access qualifier,X 可以縮小為 this,或是放大到 package。所以我們可以用這個方式來定義,package private 或 package protected。

範例:限定範圍到 package
package p1 {
  package p2 {
    private[p1] class C1 // class C1 為 private,且可以 access 的範圍擴充到 package p1
    private[p2] class C2 // class C2 為 private,且可以 access 的範圍擴充到 package p1.p2
  }
}
範例:限定範圍到 outer class
class Outer {
  class Inner {
    protected[Outer] def p1 = println("p1") // 限定的範圍擴充到 Outer
  }
}
範例:限定範圍到 class
class Outer {
  class Inner {
    protected[Outer] def p1 = println("p1") // 限定的範圍擴充到 Outer
  }
}
範例:限定範圍到自己這個 instance。
這是 Java 沒有的限制方式,表示本 field,只有自己 instance 可以 access,就算相同的 class 的 instance 也不允許 access。此種限制方式稱為「object-private」,是最嚴格的限制
class S {
  private[this] val n_object_private = 10 // 這個是 object private
  def p = {
    val s = new S // create 另一個 S instance
    val n = s.n_object_private // 錯誤,因為在本 method 底下,不允許 access 別的 instance 的 object private field
  }
}

6. Companion object 與 class 的 access right。
Companion object 與 class 這兩個東西,在 Java 世界中其實是一個 class,為了簡化方便,Scala 將一個 class 創造出兩個 class 出來。
既然原先在 Java 中本來是一個 class,所以二者本來就是一體,所以有兩個結論

a. Companion object 與 companion class 彼此可以互相 access private 的部份。

b. Companion object 與 companion class 的 access right 是一致的。也就是,若有一個 class X 允許讓 class C access,則 object C 同樣也 access X。

7. Package 沒有 access control 的機制,也就是 package 一定都是 public 的。

8. class 宣告也可以加上 private 或 protected 以做進一步的控制。
protected 的 class 表示該 class 只能給同一個 package 或 sub_package 的其他 class access。
private 的 class 表示該 class 只能給同一個 package 的其他 class access。
protected[this] 的 class 表示該 class 只能給包起來的 package 的其他 class access。
private[this] 的 class 表示該 class 只能給包起來的 package 的其他 class access。
範例
package p {
  private class C { // 宣告 package p 裡面有一個 private class C
  }
}

5-8. Abstract class

Abstract class 代表的是該 class 是一個抽象的 class,所以不能把該 class 直接 create instance。
Abstract class 通常會包含多個 abstract method,但也可以沒有 abstract method。沒有 abstract method 的 abstract class,通常代表該 class 不是 concrete class,希望使用 subclass 來 create instance。

比如我們有一個 Animal class,但在我們應用中,Animal 只是用來表明所有的動物的 superclass,Animal 並沒定義有 abstract method,此時我們仍會將 Animal 標注為 abstract class。
所以 abstract class 主要是要看它是否代表抽象而言,而非以有無 abstract method 來斷定。當然有 abstract method 的 class 就一定是 abstract class。

在 Java 中,abstract class 需要在 class 前以 abstract modifier 註明該 class 是 abstract class。而 abstract method 也需要以 abstract modifier 註明該 method 是 abstract method。
範例
abstract class C { // 用 abstract 註明本 class 為 abstract class
  abstract void m1(); //用 abstract 註明本 method 為 abstract method
}
當有 subclass 繼承某個 abstract class 時,需要 implements 所有的 abstract method,否則表示該 subclass 仍有尚未 implement 的 abstract method,這時該 subclass 仍需宣告為 abstract class。

Scala 的 abstract class 的概念與 Java 的類似。也是使用 abstract 宣告。
但有點不同之處,Scala 中不需特別宣告 abstract method,也不能宣告某個 method 為 abstract method。只要有某些 method 沒有定義,那些 method 自動會被 Scala 標示為 abstract method。

所謂 method 沒有定義的意思是,method 宣告後沒有對應的程式碼(即沒有使用等號,或 method 後面沒有使用大括號括住的程式碼),就是 method 沒有定義。

範例
abstract class S { // 用 abstract 註明本 class 為 abstract class
  def p: String // 宣告一個 method p,但沒有定義,所以 p 是 abstract method
}
val s = new S // 這是錯誤的,因為 abstract class 不能 create instance

5-7. Anonymous class

Anonymous class 就是不具名的 class,通常用在我們不需要給一個很清楚的 class 名稱時。

在 Java 我們時常遇到 anonymous class,anonymous class 讓我們很方便產生一個暫時性的 class。
Java 產生 anonymous class 的語法,通常隨著 create 一個 anonymous class 的 instance 一起。
範例
Runnable r = new Runnable { //這裡表示產生一個 anonymous class,但implements Runnable interface
                            //大括號裡面就是該 anonymous class 的內容
                            //定義完 anonymous class,馬上 create 一個 instance 傳回去
      public void run() {
         ...
      }
    };
Thread thrd = new Thread(r).start();
  });
或許語法,你不會很熟悉,但這是很常見的寫法,請把它記住。

在 Scala 中,anonymous class 的用法更多,通常也是跟隨著要產生一個特定型態的 instance。
Scala 中的語法與 Java 的類似。
範例
val v = new { // new 後面不需要有 type 名稱,直接可以宣告 anonymous class 並 create instance
          def p = println("P")
        }

Anonymous class 一個很好用的工具,雖然導致出來的語法好像有點掉頭掉尾,但我們應該好好使用,可以讓你得成是寫起來更快速,更精簡。

5-6. Inner class

Inner class 在 Java 中時常被使用,在 Scala 中也一樣很常被使用。

Java 宣告 inner class 的方式,是在 class 中 宣告另一個 class。
Inner class 被編譯後,會產生 outer_class$inner_class 的 classfile。
甚至在 method 中也可以宣告 inner class。

範例:宣告 inner class
class J1 {
  class J11 { //inner class
    void print() {
      System.out.println("J11");
    }
  }
}
上例會產生兩個 class file
1. J1.class
2. J1$J11.class:代表 J11 的 inner class。

範例:在 method 中宣告 inner class,此時只有該 method 才知道這個 inner class。
class J1 {
  void p1() {
    class J11 { //method 中 的 inner class
      void print() {
        System.out.println("p1");
      }
    }
  }
  void m1() {
    J11 j11 = new J11();//這裡是錯誤的,因為 m1() 看不到 J11
  }  
}
上例會產生兩個 class file
1. J1.class
2. J1$J11.class:代表 J11 的 inner class。
但由於 J11 是宣告在 J1.p1() method 裡面,所以在 m1() 中將看不到 J11 這個 class。

Inner class 的 instance 隱含有一個 outer class instance 的 reference。也就是,若要 create 一個 inner class instance,需要有一個 outer instance 才能成功 create。若在其他 class 要 create inner class 的 instance,需要使用特殊的語法。
範例:inner class instance 的 creation。
class J1 {
  class J11 { //inner class
    void print() {
      System.out.println("J11");
    }
  }
  void m1() {
    J11 j11 = new J11();//這裡 create 一個 J11,好像沒有 outer class instance
//但其實在 instance method 中呼叫,已經隱含一個 outer class instance。
  }
  static public void main(String[] args) {
    J11 j11 = new J11();//這裡會出錯,因為 J11 的 instance 需要 outer class instance
    J1 j1 = new J1();
    J11 j11 = j1.new J11();//這裡使用 j1 來 create J11。
//請注意 j1.new J11() 這個特殊語法
  }
}

Scala inner class 用法,與 Java 的類似。我們看例子。
範例:宣告 inner class
class S1 {
  class S11 {
    def p1 = println("p1")
  }
}
上例會產生兩個 class file
1. S1.class
2. S1$S11.class:代表 S11 的 inner class。

請注意,Java 的 inner class 的實際 type,為:outer_class.inner_class,使用「.」dot。
但 Scala 的「.」dot,只能使用在 instance 上。compiler 一遇到 dot 就會試圖解釋為 instance 的 method 或data field。
所以,在 Scala,inner class 的實際 type,為 outer_class#inner_class。Scala 與 Java 不太相同,請注意。
class S1 {
  class S11 {
    def p1 = println("p1")
  }
}
object Main {
  def m1 {
    val s11: S1.S11 = null // 使用 S1.S11 當 type,但這是錯誤的。遇到「S1.xxx」時,compiler 會去找 S1 這個 object
    val s12: S1#S11 = null // S1#S11 才是正確的 type。
  }
}
範例:在 method 中宣告 inner class,Scala 的用法與 Java 相同。
class S1 {
  def p1 = {
    class S11 { // 在 method 中宣告 inner class
    }
  }
}
與 Java 相同,Scala inner class 的 instance,也是隱含一個 outer class 的 reference。所以要 create 一個 inner class 的 instance,需有一個 outer instance。
使用 outer instance 來 create inner instance 的語法,Scala 與 Java 不太相同。
class S1 {
  class S11 // 這個 class 沒有 body,所以可以省略大括號
}
class Main {
  val s1 = new S
  val s11 = new s1.S11 // 這裡的語法與 Java 不同,new 為 keyword,放在最前面
}
在 Scala 中,不同的 outer instance 所產生出來的 inner instance,type 是不相同的。
讓我們看下一例子。
class S {
  class Inner;
}
class Main {
  val s1 = new S
  val s1_inner = new s1.Inner  //s1_inner 的 type 為 s1.Inner
  val s2 = new S1
  val s2_inner = new S2.inner //s2_inner 的 type 為 s2.inner
}
上例的 s1_inner 與 s2_inner 都是 S#Inner 的 type。
但實質上,二者的 type 並不相同,一個是 s1.Inner type,另一個是 s2.inner type。這種與 outer instance 相關的 type,稱為 path-dependent type。

5-5. package

Java 使用 package 來組織我們的程式。我們將程式區分成各個 module,然後依照彼此相依的程度,放到不同的 package 中。Java 使用 package 來命名不同的 namespace。

使用 package 的主要用意有兩個
1. 組織我們的程式碼
2. 避免名稱衝突:比如你自己的程式有個 class A,你使用的 library 也有個 class A,那就麻煩。

Java 建議使用 domain name 的倒裝來命名你的 package。比如,昇陽的 domain name 為 sun.com,所以會以 com.sun.xxx 來當成 package 名稱。

基本上,domain name 倒裝的建議是滿好的,但卻會造成 package name 過長的情況。比如,我們常用的 Apache Log4J,它的 configure 的  package 名稱為 org.apache.log4j.config,夠長。你會希望你自己公司內部的 Java 程式使用這麼長的 package name?因此,通常,我們並不一定會如此命名。

其實,domain name 倒裝主要的因素是為避免名稱衝突,所以一般用在會 release 給不特定用戶的 library 上。若是你的程式只是自己公司內部使用,該程式不準備 release 給別人使用,這時,並不需要如此嚴謹,僅需要按照部門或是其他功能別,做適當的 package 命名即可。

關於 Java 的 package,有一點可能有一些人誤會,那就是 package 與 sub-package 的關係。比如「org.apache.log4j」與「org.apache.log4j.config」,我們有時會說後者是前者的 sub-package。
但,說 sub-package 其實並不合適,因為 Java 對於所謂的 package 與 sub-package 二者是完全無關的,也就是 p1 ,與 p1.p2 兩者可以視為是完全獨立的 package。p1 與 p1.p2 的關係,就如同 pa 與 pb 之間的關係一樣,兩者是完全獨立的,沒有所謂的 p1.p2 繼承 p1 的這件事,請大家要小心。
這個原則不僅在 source code 上有效,在編譯完後的 classfile 也同樣是成立的。

Java 使用 package 這個 keyword 宣告該檔案裡面所有 class 歸屬的 package。
要注意的是, Java 中的 package keyword 需要是該檔案的第一個有效的 statement,否則 compiler 會視為錯誤。Java 檔案只能有一個 package 的 statement。

範例:Java 中使用 package 宣告 package name。
package p1.p2;//宣告本檔案屬於 p1.p2 package
class J1 ...
class J2 ...

Scala 延續 Java package 的概念,所以我們可以在 Scala 繼續使用 package 的概念,但 Scala 稍微做了一些的修改。

在 Scala,我們可以繼續使用 Java package 的宣告方式,使用這種方法,在該檔案裡面宣告的 class 都隸屬在該  package 裡面,與 Java 的使用方式一模一樣。
範例:Scala 中使用 Java 方式的 package 宣告。
package p1.p2
cass S1 ...
class S2 ...

Scala 的 package 命名,也可使用大括號將 package 的範圍刮起來,這種方式稱為 packaging。
範例:Scala 中使用 packaging 的方式宣告 package。
package p1.p2 { // 使用大括號,宣告 p1.p2 的範圍
  class S1
  class S2
}

Scala 的檔案,可以允許有多個 package,也可以在 package 中宣告 sub-package。
範例:一個檔案中有兩個 package
package p1 { // 宣告 p1 的範圍
  class S1
}
package p2 { // 宣告 p2 的範圍
  class S2
}
範例:package 裡面宣告 sub-package
package p1 { // 宣告 p1 的範圍
  class S1 // S1 是在 package p1 裡面
  package p2 {
    class S2 // S2 是在 package p1.p2 裡面
  }
}
範例:package 裡面宣告 sub-package,若裡面只有一個 sub-package,可以省略大括號
package p1 // p1 裡面只有一個 p2,所以大括號省略
  package p2 {
    class S2 // S2 是在 package p1.p2 裡面
  }
範例:package 裡面宣告 sub-package,若省略大括號,可以將 sub-package 往左移動,會比較好看
package p1 // p1 裡面只有一個 p2,所以大括號省略
package p2 { // p2 往左移,看起來猶如連續宣告兩個 package,這種連續宣告 package 的方式,稱為 chained-package clauses
  class S2 // S2 是在 package p1.p2 裡面
}

在上例這種 package 中,包含 sub-package 的宣告方式,Scala 允許 package 與 sub-package 間有更彈性的使用方式。
但請注意,這只是 Scala 的 syntax sugar,就如同前面提的,編譯後的 package 與 sub-package 彼此完全獨立。所以或許使用 inner-package 的名稱會比 sub-package 的名稱好,以下將使用 inner-package 這樣的名稱。
範例:inner package 可以直接使用 outer-package 的 class
package p1 {
  class C1
  package p2 { //這個 package 實際代表 p1.p2
    class C2 {
      val c1 = new C1 // inner-package 的 class,可以使用 outer package 的 class,而不需使用 full-path class name
      val c11 = new p1.C1 // 此為完整的 access 方式,上一個 statement 為 syntax sugar
    }
  }
}
範例:outer package 可以使用相對路徑 access inner-package 的 class
package p1 {
  class C1 {
    val c2 = new p2.C2 // 使用相對路徑 access inner package 的成員,此仍為 syntax sugar
    val c22 = new p1.p2.C2 // 此為完整的 access 方式
  }
  package p2 { //這個 package 實際代表 p1.p2
    class C2
  }
}
範例:若 inner package 與 outer package 分開寫,syntax sugar 不再啟動,需使用絕對路徑 access 方式
package p1 {
  class C1 {
    val c2 = new p2.C2 // 使用相對路徑,這是錯誤的
    val c22 = new p1.p2.C2 // 此為完整的 access 方式
  }
}
package p1.p2 {
    class C2 {
      val c1 = new C1 //使用相對路徑,這是錯誤的
      val c11 = new p1.C1 // 此為完整路徑,這裡才是正確的
    }
  }
上例中,package p1 與 p1.p2 回復 Java 原有的規則,彼此之間互相獨立,所以彼此都需要使用絕對路徑來 access。

Scala 定義一個東西,用來表示最外層的 package,此為 _root_,所有 top-level 的 package 都是_root_的 sub-package。
注意:_root_ 只能用來 access 最上層的 package,但不能用來 access 最外層 class。
範例
package p1 {
  class C1
  package p2 {
    class C2 {
      val c1 = new C1 //使用相對路徑,使用到 p1.C1
      val c1_top = new _root_.C1 // 使用 _root_ access 來 access 最外層的 class C1,錯誤,因為 C1 不是 _root_ 的 element
      val c1_top2 = new _rrot_.p2.C1 // 使用 _root_ access p2.C1
    }
  }
}
class C1
package p2 {
  class C1
}
上例中,我們希望在 p1.p2.C2 access 最外層的 p2.C1,此時需要使用 _root_。
但 _root_ 無法用來 access 最外層 package 的 class。

看到這,你是否覺得 Scala 的 package 機制已經過於複雜。若你的程式在這種想細節上玩把戲,筆者認為是不智的。
使用 package 包 inner package,再加上使用相對路徑的方式,只會讓你的頭腦變暈,建議不要這麼複雜,回歸 Java 一個 package 的寫作方式,若有需要運用別 package 的 element,再使用 import statement 就好了。當然一個檔案包含多個 package 的作法到是不需丟棄,可以好好運用。

Scala 雖然對 package 做了修正,但對於 Java 對 package name 使用 domain name 倒裝的建議,仍然維持。

5-4. 檔案與 class 的關係

在 Java 中,source code 的檔案名稱,以及目錄階層,與你的 class name 與 package 需要相對應。這是你在撰寫 Java 程式時需要注意的。

Java 規定 public class 需要放在與 class 同名的檔案中,目錄結構需要與 package 結構相同。

這樣規定的好處,是可以一目了然,看出原始碼的結構。但壞處是,常常我們會發現,Java 的檔案數很多,目錄層次也很多。最常感到無奈的是,你無法在一個 Java 原始檔案中看到兩個 public class,常常為了一個新的 public class,需要再產生一個 Java 檔案。

不僅僅 source code,甚至所編譯出來的 class file,Java 也會按照 package 的方式,將 class files 放置到對應 package 的目錄中。JVM 因此可以按照目錄,找尋到所需要的 class。

這種與檔案結構相對應的構想,有好也有壞,我們不適合去評判。

在 Scala 中,已將這樣的規定移除。在 Scala 程式中,你可以在一個檔案中宣告多個 class,甚至這些 class 是不同的 package。Package 命名也不再與目錄層次相對應。

將這種規定,讓人有鬆綁的感覺,確實適合 Scala 宣傳其快速開發的格調,但好像就比較不嚴謹。

可是雖然 Java 嚴謹,但這樣命名的規定,相信大多數人卻也看不出實質好處在何處。所以,Scala 移除這樣的規定,對大部分人而言,應該是好事一樁。

雖然,Scala 把 class 命名與檔案目錄對應的規定移除,但這只是 source code 的部份,對於編譯出來的 classfile,Scala 仍依循 Java 的規定,否則 JVM 該如何找到所需的 class 呢?這是你該曉得的。

5-3. Instantiation

有了 class 的宣告,如何 create 該  class 的 instance?
這個 create instance,在 OO 的術語叫做 instantiation,相信你知道。

Java 中,要產生一個 class 的 instance,使用 new class_name(param...) 的方式來產生 instance。
Scala 的方式,與 Java 一模一樣,也是 new class_name(param...)。
其中的 param 是參數,表示要傳給 constructor 的參數列。
例:
val s1 = new S() // create 一個 S instance, constructor 沒有參數
val s2 = new S(1, "abc") //// create 一個 S instance, constructor 的參數為 1 與 "abc"
Scala 有一點點與 Java 不同的,就是當 constructor 沒有參數時,可以省略小括號。
這個我們可以簡稱為「小括號省略原則」。
「小括號省略原則」,我們在討論 method 呼叫時,也會看到,這是一個相當重要的原則。

例:
val s = new S // 當 S 的 constructor 不需要有參數時,可以省略小括號

對 Java 熟悉的你,相信 instantiation 不會是件困難的事。

5-2 . Scala object hierarchy

我們首先來談,OO 語言中,最基礎的 object 的架構。

Scala 與 Java 一樣,為避免多重繼承所引起的問題,以及簡化語言的複雜性,採取單一繼承的方式,這也是許多程式語言的方式。

其實,更嚴格來講,不是 Scala 採取與 Java 相同的單一繼承方式,而是「Scala 繼續使用 Java OO 的架構」,這樣講更精確。因為 Scala 根本沒有離開 Java 的範疇。

之前,我們曾提過,Scala 可以與 Java 混用,混用的基礎,除了 Scala 編碼出 JVM 的 bytecode 外,更重要的是,Scala 繼續沿用 Java OO 架構。
我們可以想像「Scala 是 Java 的另一種語法」,這句話是確實的,而且讀者應該把它聽進去。Scala 在 Java 原來架構中擴充,以 JVM 的角度,兩者是混雜在一起。以程式語言的角度, Scala 只是擴充 Java。

我們來看如何擴充?

Java 除了 object 外,為了效能,也支援 8 種支援 primitive type。所以,在 Java 你需要同時面對 primitive type value 與 object。
下圖,是 Java 關於資料型態的示意圖。



我們提過,Scala 為純 OO 的語言。但這不代表 Scala 的效能就不好,或是 Scala 去除掉 Java 的 primitive type。

Scala 是 Pure OO 語言,所以有一套完整的繼承體系。為兼顧效能的問題,Scala 特別為值相關的整合在 Value 值的子系中,而將一般非值的放入 Reference 的子系。Compile 時 compiler會特別為值子系的 instance 編譯為 JVM 對應的資料。

因此,Scala 的手法是包裝,把 Java 的 primitive type 使用 class 把它包裝起來,讓這些 primitive type value,在 object hierarchy 中出現在該出現的地方。

在 Scala 中,把所有的 primitive type 使用一種特殊的 class 包裝起來,這個特殊的 class,在 Scala 稱為 value class。所有 Java 的 primitive type,在 Scala 中都有對應的 class。Value class 有一個共同的 parent class,稱為AnyVal。也就是 AnyVal 是所有 value class 的 root class。

但請注意,這些 value class 是 Scala 世界才可以看到,compile 完後,會變成 Java 的 primitive type,因此不會有效能的問題。我們可以說這些 value class,對應到 Java 的 primitive type。

Scala 的 value class,有一個較特殊的,叫 Unit,我們之前曾經討論過。Unit 是 Java 的 void 對應,但它是一個 value class,請大家不要搞混了,以為它也是一個 reference class。

同樣的,Java 的 Object class,在 Scala 中有一個對應的 class 叫做 AnyRef,代表所有 reference 的 root class。你可以想像,Java 的 Object 就是 AnyRef。在 Scala 世界,Object 這個 class 不再是 object hierarchy 的 root class。

看到此,我們知道,Scala 將 object hierarchy 分成兩大支,一個是 AnyVal,一個是 AnyRef。很顯然,Scala 已經將原來的 object hierarchy 架構擴充開來。AnyRef 這個系列就是原來 Java 的 object hierarchy,而 AnyVal 這個系列就是原來 Java 的 primitive type(再加上 void)。

為整合 AnyRef 與 AnyVal 這兩個大的子系,有一個 Any 的 class 出現,這個 class 是 AnyVal 與 AnyRef 的 parent class,這個 Any 才是 Scala 中的 root class。

上面我們使用「Scala 的什麼東西,對應 Java 的什麼東西」,「對應」這兩個字,其實要更強化一點,對應的意義其實是「把它們當成相同」,我們再進一步解釋:
1. Java 的 int,在 Scala 使用 Int 對應:這代表「Scala 的 Int 就是 Java 的 int,只不過 Scala 把它包裝起來」。同理,所有的 primitive value class 都可這樣看待。
2. Java 的 void,在 Scala 使用 Unit 對應:這代表「Scala 的 Unit 就是 Java 的 void,只不過 Scala 把它包裝起來」。
3. Java 的 Object,在 Scala 使用 AnyRef 對應:這代表「Scala 的 AnyRef 就是 Java 的 Object,兩者可視為同義」

最後我們將 Scala object hierarchy 整理如下圖:



在上圖,可以發現有兩個 class,Null 與 Nothing。Scala 把這兩個 class 加入的原因,是要將 object hierarchy 弄得更完整。

在 OO 語言中,parent class 的變數,可以使用 subclass 的 instance 來指定。所以在 Java,我們可以將任何 instance 指定給 Object 變數,比如,下例是合法的。
Object obj = "abc"; // obj 為 Object 類型的變數,可以使用 "abc" 指定

在 Java,我們可以把 null 值設給 Object 變數。在 Java 中,null 被當成很特殊的 instance 值,所以我們可以「把 null 值設給 Object 變數」,這算是一個特殊規定。

Scala 不希望依循特殊規定, Scala 希望依循的是 OO 的規範。若要達到「讓所有 AnyRef 的變數可以設定為 null」,則 null 必須有某種特性在。注意,在 Scala 中所有東西都是 object,null 也是一個 object。

「讓所有 AnyRef 的變數可以設定為 null」,表示 null 必須是一個特殊 class 的 instance,而該 class 是所有 AnyRef class 的 subclass。

為符合這樣 assignment 的原則,Scala 定義一個新的 class 叫 Null,這個 Null class 是所有 AnyRef subclass 的 subclass。Null class 只有一個 instance,叫做 null。如此一來,null 可以很順利的指定給任何 AnyRef 的變數,而不會破壞 OO 的理論。

請注意,Scala 的 null,不像在 Java 中是一個值,在 Scala 中,它是一個 instance。

Null 是所有 AnyRef subclass 的 subclass,Scala 也針對 Any 這個 class 定義了一個 class,叫 Nothing class,該 class 是所有 Any subclass 的 subclass,這樣整個 Scala 的 object hierarchy 就完整了。
但請注意,Nothing 沒有 instance,因為 AnyRef 的變數 可設定為 null,但 AnyVal 的變數一定是有值,所以不需設為 nothing 這樣的東西,所以 Nothing 並沒有存在 instance 的必要。

Null 與 Nothing 稱為「底層型態」(Bottom Type)。

我們稍微整理一下,Scala 繼續使用 Java 的 object hierarchy,並沒有改變。但 Scala 將原來 Java 的 primitive type 與 object type 加以整合,讓你可以使用純 OO 的角度來看 Scala 的世界。

但請注意,這些擴充與整合,都只出現在 Scala 的世界,並不因此改變 Java 的東西,Java 的東西完全沒有做任何更改。
當 compile 完成,這些 Scala 所擴充的出來的東西,都會編譯為原來 Java 該有的面貌,完全不會改變 Java 的世界。

由於 Scala 只是延續 Java 的架構,並沒有破壞 Java 的東西,所以 Scala 可以與 Java 混合得這麼成功,也因此,我們常說,我們可以把 Scala 看成開發 Java 程式的另一個方法。

5-1. Single inheritance

Scala 與 Java 一樣,為避免多重繼承所引起的問題,以及簡化語言的複雜性,採取單一繼承的方式,這也是許多程式語言的方式。

但多重繼承並不完全是缺點,它仍是很合乎人們的思考方式,為改善這個缺陷,Java 引進 interface 的觀念。
Scala 中不再支援,但支援更強的功能,叫做 trait(特徵)我們之後會好好解釋 trait。

5. 再論 class

在第 3 章我們討論 class 與 object 的宣告,這個章節我們將延續這個主題,繼續說明與 class 相關的議題。

2011年5月6日 星期五

4-8. Scala 語法整理

到此處我們已經完成 Scala 的控制結構部份。

整理一下 Scala 的語法,你會發現它其實並不算困難,甚至 Martin 想要盡量簡化。

Scala 主要的語法包含以下各項
1. class 宣告:包含 class / object、data field、method 宣告。
2. instantiation:create instance。
3. Local 變數 / 常數的宣告。
4. assignment:指變數的 reassignment。
5. 控制結構:if、while、for、try、match。
6. method / function call:用來呼叫 object 的 method。
雖然還有一些次要的,但主要的就是這些,算簡單了吧!

值得一提的是,method call 會運用「小括號省略原則」,該原則未來會詳述,這裡大約提一下。method call 原來的格式是「object.method(param1, param2)」,但若參數是零個或一個,可以省略 dot 與小括號,這就是「小括號省略原則」。

重點是,Scala 中並沒有像其他語言有的「關係運算式」,以及「加減乘除」等運算式的 statements。

這裡不是說,Scala 不能做這些運算,而是 Scala 很巧妙的,將這些運算整合到 method call。
舉例而言:
1 + 2 的句子其實是 1.+(2) 的 method call
1 < 2 的句子其實是 1.$less(2) 的 method call

所以 Scala 並不需要有這些運算式的語法,全賴 method call 就可以。compiler 只需運用 method call 的技巧,不需特別為「關係運算式」與「加減乘除」等運算式設計 parser 與 compile。
所以說,Scala 的語法根本沒有這些運算式,這是大家要記住的。很巧妙吧!

這個概念我們未來會仔細討論,在這裡提到只是要告訴大家,Scala 的基本語法並不難,甚至算簡單,只要你把 Scala 的原理搞清楚,也許你反而會覺得 Scala 簡單。

雖然我們說 1 + 2 其實是 1.+(2),但由於 Int 對應到 Java 的 int type,所以 Scala 中,Int 的 + 這個 method 會有特別的 implement 方式,compiler 編譯時會直接將該 method 轉換成 Java 的 int 的運算式,所以大家不用擔心效能的問題。我們可以稱這種 method 是使用 「primitive operation」
來 implement。

結束了控制結構,下個章節我們將回頭繼續討論 class。

4-7. 控制結構 --match expression

如同在「Scala 的企圖心,也是它的特性 --Pattern Matching」中提到的,Scala 的 pattern match 的來源是來自於函數式語言的參數比對。

Match expression 有時會叫 pattern match,是一個非常繁雜的東西,在這裡我們無法全面將 match expression 講清楚,待一些相關的知識補足之後,我們會回頭再將說明仔細。本篇只會說明 match expression 的意義,以及大致的用法。

在「Scala 的企圖心,也是它的特性 --Pattern Matching」我們提及,pattern match 用在參數比對,找尋一個符合的 function 定義。
那時的例子我們定義一個 fib 函數,但依照參數不同,有不同的定義。同一個函數,有多個定義,這種方式通常會出現在純函數式語言,讓你用「比較像數學的方式」來定義函數。

在非純函數式語言,通常不使用同一函數多重定義的方式,而是在該函數定義中,直接對參數做分類,將符合的程式碼撰寫在比對之後。Scala 就是使用這樣的方式。

讓我們看例子會比較清楚。
原來在該文中,費伯納西數列的例子如下:

function fib(1) = 1
function fib(2) = 1
function fib(n) = fib(n - 1) + fib(n-2)

在 Scala 我們使用 match expression,將三個情況寫在同一個 function 中。
在 Scala 的對應程式碼如下
def fib(n: Int): Int = n match { // fib 因為使用到 recursion,所以 fib 函數需要宣告 return type
  case 1 => 1
  case 2 => 1
  case _ => fib(n - 1) + fib(n - 2)
}
我們仔細與前面 function fib 程式碼比對,是不是除了把三個函數寫在一起外,其他的部份幾乎一模一樣。
我們看實際執行情況


前面程式中的 n match {...} 就是 match expression。

Scala 中 match expression 的格式:var_name match {
  case match_1 => match_1 相對應的程式碼
  case match_2 => match_2 相對應的程式碼
  ....
  case _ => 所有都會對應的程式碼
}

我們把 match expression 的重要部份,一個個說明如下

  1. match expression 使用 var_name match 帶頭,後面加上一個使用大括號括起來的程式碼。該段程式碼由一個個 case 子句所構成。
  2. 一個 case 子句,代表一個比對。case 子句比對的對象,沒有限制一定是要整數,任何型態都可比對,甚至我們可以比對不同的型態。
  3. 上例中,最後一個 case,所對應的是 _,代表所有狀況都滿足。
    • underscore _ 我們之前曾經遇到過,用於變數的初始值,當時代表使用系統的初始資料。
    • underscore _  是 Scala 的「預定萬用字元」,代表的意思就是那個「預定的東西」。用在 match expression 的意思是,所有的東西都與 _ underscore 符合。若以 Java switch 來看,就是那個「default」的 case。
  4. match expression 比對的方式,是由第一個 case 開始比對起,一直往下比對,直到比對到一個符合的 case。找到符合的之後,就不會再比對之下的 case。
  5. 找到符合的 case,會執行對應的程式碼,然後結束。
  6. 若沒有找到符合的 case,會產生 MatchError。
  7. Match expression 的 result value,是最後一個執行到的 statement,也就是符合的 case 子句的  result value。
範例:沒有符合 case 的情況,產生 MatchError

Match expression 不僅僅可以比較值,也可以根據不同的參數型態,做不同的事情。
範例
def f(x: Any) = x match {
  case n: Int => println("Int! n = " + n) // 若是 Int
  case s: String => println("String! s = " + s) // 若是 String
  case _ => println("Unknown! x = " + x)
}
實際執行情況

你若熟悉 Java 應會為上面的例子,感到震驚,實在是強了!

讓我們舉最後一個例子,同樣出現在「Scala 的企圖心,也是它的特性 --Pattern Matching」中。

假設你要寫一個 greeting 的功能

1. 對於你的好友 John 要說 "John 你好"
2. 對於女士要說 "Miss " + name + " 你好"
3. 對於男士要說 "Mr " + name + " 你好"
4. 對於鴨子要說 "呱呱"

當時函數式語言的舉例寫法

function greeting("John") = "John 你好"
function greeting(isMiss(name)) = "Miss " + name + " 你好"
function greeting(isMr(name)) = "Mr " + name + " 你好"
function greeting(duck: Duck) = "呱呱"

使用 Scala 我們會如何做呢,以下就是範例
//先準備 isMiss 與 isMr function
def isMiss(name: String) = name match {
  case "Mary" => true // 這裡我們只 demo Mary 是 Miss 的情況
  case _ => false
}
def isMr(name: String) = !isMiss(name)

// 定義 Duck class
class Duck 
// 定義 greeting
def greeting(x: Any) = x match {
  case "John" => "John 你好"
  case x: String if isMiss(x) => "Miss " + x + " 你好"
  case x: String if isMr(x) => "Mr " + x + " 你好"
  case duck: Duck => "呱呱"
}

//測試
println(greeting("John"))
println(greeting("Mary"))
println(greeting("Jason"))
println(greeting(new Duck()))
實際執行


執行結果,就如同我們想像的一樣,這個 match expression 真的不愧有殺手級的能力。想想看,如果使用 Java 要做到功能,你要花多少功夫?!

既然 match expression,這麼的強大,想當然爾,Java 的 swtich 就沒有留存的必要,所以,在 Scala 中很自然的被移除了。

這樣功能的 match expression 你滿意嗎?可是還沒完,match expression 還有更好用的能力,未來我們會更加詳述。

2011年5月5日 星期四

4-6. 控制結構 --try,例外處理

例外處理幾乎是現代語言所必備,很重要,你一定會使用。
既然大家都熟悉,所以這裡不準備解釋例外處理的緣由,以及發生例外時的處理流程。

Scala 的例外處理與 Java 的例外處理大致類似,都是 try -- catch -- finally 所組成。
Scala 例外處理的語法如下
try {
  val socket = new Socket("www.a.com")
}
catch {
  case ex: IOException => ... //處理 IOException 的程式碼
  case ex: Exception => ... //處理 Exception 的程式碼
}

在例外處理這個議題上,Scala 與 Java 不同的地方有以下幾點:
1.catch 子句把所有的 exception 放在同一個 catch 子句處理。不再像 Java 一樣,一種 exception 需要一個 catch 子句,看起來比較簡潔。
2. catch 子句使用 pattern match 的語法。exception 發生時,會依序比對 case 子句,看哪一個符合,若符合,執行對應的程式碼,表示該 exception 被處理完成。pattern match 將在下篇中提及。
3. Scala 不再支援 checked exception:這是 Scala 與 Java 最大不同的地方。

所謂「checked exception」是 Java 的特殊設計,基本概念是要求 programmer 一定要處理掉程式中可能遇到的所有 Exception,若沒有處理,compiler 會發出 error。

在「checked exception」的原則下,method 要使用 throws 宣告自己可能產生的 exception。呼叫其他 method 時,被呼叫 method 的可能 exception 都需使用 catch 處理。若有某些 exception 不處理,表示本 method 執行時可能會產生這些沒處理到 exception,因此需要在自己的 method 使用 throws 宣告這些沒處理到的 exception,告知若有人要呼叫到本 method 者記得繼續處理這些 exception。

「checked exception」的處理對象是 Exception 以及其 subclass 的例外。若程式的例外情況不是 Exception 產生,那不會運用「checked exception」。比如例外情況是 Error 產生,compiler 並不會強制要求程式碼處理這種例外。

在「checked exception」原則下,你撰寫 Java 程式,一定常遇到 compiler 要求你加上某些 exception 的處理。這時你是感到 Java 很窩心,還時覺得很煩?若你感到很煩,很正常。

「checked exception」設計的出發點是好的,但實際使用時,常形成以下狀況
1. 造成沒處理的 exception,傳染到所有的可能用到的 method,導致這些 method 都需要使用 throws 宣告該 exception。這個現象以 IOException 最普遍與著名。
2. 為避免麻煩,programmer 乾脆在每個 method 內直接攔截所有的 exception,造成 checked exception 的原意盡失。
3. Programmer 花太多時間處理這些例外的狀況。

關於 Java 的 checked exception,論戰已久,Java 設計者認為這是一個輔助 programmer 的好工具,但實際應用卻不盡理想,因此 Scala 決定將這個概念移除。Scala 中,沒有所謂的 checked exception。

Scala 中,若有一些 exception 沒有處理,該 exception 就會往上層 method 傳遞。Compiler 不會檢查有哪些 exception 沒被處理。所以你只需要針對你想處理的 exception 處理就好,其他的 exception 可以完全不管。

若你的 Scala 程式碼想要給 Java 程式呼叫,也想要製造 checked exception 的效果,這時可以使用 @throws annotation 來達到。
下例的 print method() 會有 IOException 的 throws 宣告
throw exception
class S {
  @throws(classOf[IOException]) //這個 annotation 宣告 throws IOException
  def print() = ...
}
以下是實際的 compile 的結果
由上例,使用 javap 可以看到 p1 method 使 throws 子句,宣告該 method 產生 java.io.IOException。

我們使用 try ... catch ... finally 來處理 exception,那我們如果想在程式中產生一個 Exception 該如何?

在程式中拋出一個 Exception,作法與 Java 一樣,使用 throw 這一個 keyword。
以下為一簡單例子
throw new IOException("file not found") // 這個是 Scala 程式碼,雖然與 Java 程式碼相同

「Scala 所有 statement 都有 result value,那 throw statement 的 result value 什麼?」

throw statement 執行時,程式將會由該 statement 跳離,所以理論上,throw statement 的 result value 無法被取得,所以無意義。Scala 定義 throw statement 的 result value 是「沒有東西」。為特別表示這個「沒有東西」,特別定義一個 「Nothing」 這個  class 來表示,Nothing 這個 class 並沒有 instance,不像 Unit class 有一個 instance,為 ()。

「為何不使用 Unit 來作為 throw statement 的 retsult?」
其實,Unit 是有特殊用法,它用來與 Java 溝通,Java 的 void 在 Scala 的表示就是 Unit,所以 Unit 不能拿去給其他地方使用。我們可以想像「Unit 就是 Java void 的 class」,Unit 有相當重要的功用在。

Nothing 表示不可能有值,所以叫做「沒有東西」。不是代表「沒有值」,沒有值指的是 void,在 Scala 中使用 Unit 來表示。

最後,我們談一下 finally 的用法。

finally在 Scala 與在 Java 的用法一模一樣,使用 finally keyword 然後接上大括號所括住的程式碼。
try {
 ...
}
finally {
 //你的 finally 程式碼,通常用在 cleanup 工作上
}
請注意,finally 本意是設計來做 cleanup 處理,請不要將其他有意義的程式碼放在 finally 子句中。

try ... catch ... finally 整體是一個 statement,會有整體的 result value。
請不要把 try ... catch ... finally 分割成「try 一個 statement,catch 一個 statement,finally 一個 statement」三個 statement。但 try、catch、finally 這三個子句確實有自己的 result value。

「那整體的 result value 又是什麼?」
通常 result value 都是把最後一個執行的 statement 當成 result value。try ... catch ... finally 整體的 result value 也是同樣概念,但做了些微的調整。

在 try ... catch ... finally statement 中,不管有無產生 exception,最後一個執行的 statement 一定是 finally 的最後一個 statement。若按照「最後一個執行 statement 當 result value」的原則,finally 子句的 result value 將會是整體的 result value。

但因為 finally 主要用途是做 cleanup,不應該是正常工作的程式碼,所以使用 finally 子句的 result value 做為整體的 result value,會變成使用 cleanup 的結果,做為整體的結果。這是比較奇怪的作法。
由於上述原因,正常情況下,Scala 並不採用 finally 子句的 result value,而使用 try 子句或 catch 子句的 result value,做為整體 statement 的 result value,端視是否有 exception 產生。

我們把 try...catch...finally 執行,分成以下情況
1. 當沒有產生 exception 時,整體的 result value 就是 try 子句的 result value。
2. 若有 exception,且被 catch 所攔截下來,整體的 result value 就是該 exception handler 的 result value。
3. 若有 exception,且沒被 catch 所攔截下來:雖然會先執行 finally 再跳離,但 exception 沒被處理。這種 exception 沒被處理的情況,整體的 result value 是「沒有」,但不是 Nothing,Scala 對於這種情況完全沒定義。

不採用 finally 子句的 result value,是 Scala 的處理原則,但有一種狀況要特別小心,若 finally 子句使用 return 來結束 method,這時就有點不同了。
由於 return statement 要求將 return 的參數當成 method 的 return 值並結束,若 finally 包含 return,當程式執行到 finally 的 return 時,method 馬上會被結束並傳回。
這種現象將很容易造成誤解。請看下例
def m1() = {
  try {
    return 1 //這裡有個 return 值,但會被 finally 的 return 蓋掉
  }
  finally {
    return 2 //finally 的 return 值,決定真正 method 的 return 值
  }
}
m1() // 這裡會得到 2
如果不使用 return 的情況
def m1() = {
  try {
    1 // 這裡是 try 子句最後執行的 statement,會被當成 try...finally 的 result,最後變成 method 的 return 值
  }
  finally {
    2 //finally 的 statement,被當成 cleanup,不會成為 try...finally 的 result
  }
}
m1() // 這裡會得到 1
以上的例子,很清楚看到 return statement 的強制力,容易造成與預期不同的狀況,為避免這種問題,不使用 return 是一個好方法。Scala 建議不要使用 return statement,這裡是一個很好例證。

2011年5月4日 星期三

4-5. 控制結構 --for expression

Scala 「for」 的使用方式與 Java 有相當大的不同。

Java 中的 for statement 的主要用法
1. loop:for (int i = 0; i < 100; i++) ...,就是這種用法
2. iteration:for (String s: strings) ...,就是這種用法

Scala 的 for expression 用在 collection 的 iteration,與 Java 的 iteration 用法比較像。但由於 Scala 支援函數式的用法,所以感覺起來,Scala 中 for expression,就比 Java 的 iteration 強上非常許多。

注意:Scala 並不支援 for (int i = 0; i < n; i++) 這種以 loop 為主要思考方式的  for statement。

for 在 Scala 中使用在 iteration,也就是將 collection 中的每個 element,一個個取出來,然後執行後面的程式碼。collection 是一個放置子資料的容器,容納許多 element,Scala 中,collection 都是一種 Sequence。Scala 有時稱 for expression 為 for comprehension,或是  sequence comprehension。

所謂 iteration 就是由 collection 中取出一個 element 後,執行一次後面的程式碼,這樣稱為一次 iteration。 for expression 可以運用在所有種類的 collection 中。

for 的功用非常強大,在 Scala 中受到相當的重視,需要仔細瞭解。我們將由簡單到複雜介紹。
for 的格式是「for (x <- list) code_block」。

一.對單一 collection 做 iteration
var lst = List(1, 2, 3) //List 我們尚未介紹,請先想像它與 Java 的 List 類似
for (n <- lst) {
  println(n)
}
n <- lst,表示將 lst 中的 element 一個個取出來,每次設定給 n,然後執行後面的程式碼。
n <- lst 這個東西稱為 generator,是 for 特有的用法。

對於 n,有幾點需要瞭解
1. n 是一個 val,所以不可重新 reassign,但 n 並不需宣告 val。若程式宣告 val n <- lst,會產生 deprecation warning,也就是不希望程式再宣告 val。
2. n 不需宣告 type,compiler 會使用 type inference。但你要宣告也是可以,只是多此一舉
上例改成 for (n: Int <- lst) {....} 是合法的。

二.對一段整數做 iteration
Java programmer 喜歡的 for (int i = 0; i < 100; i++)這種模式,可使用這種 collection 來取代
for (i <- 1 to 100) {
  println(i)
}
上例 1 to 100 會先執行,得到一個 Range 的 instance,這個 Range 的 instance,可以想像是一種 collection。「1 to 100」這一個 Range instance,可以想像為「1 到 100 的 collection」。for (i <- 1 to 100),就是「1 到 100」依序放入 i 中,然後執行。

至於, 1 to 100 為何會得到一個 Range 的 instance,我們會在未來提到,這裡先不提。

三.對 Array 裡面的 element 做 iteration
對 Array 裡面的 element 做 iteration,可以使用兩種方式,第一種是使用 index 來 iteration,第二種是直接對 element 來 iteration。

1. 對 index 做 iteration
val ary = Array(1, 2, 3) // Array 尚未介紹,其實這對應為 Java 的 int[]
for (i <- 0 until ary.length) println(ary[i])
使用 index 來對 Array 做 iteration,這種方式並不好,因為由 index ,再找到 element。Scala 的建議是,直接對 element 做 iteration。

2. 對 element 做 iteration
val ary = Array(1, 2, 3)
for (elm <- ary) println(elm)

四. 過濾(filter)
若我們不希望某些 element 進去 code_block,可以事先過濾。過濾的方式使用 if。 if 是 generator 的修飾,因此是 generator statement 的一部份,所以 if 之前不需有分號。
val ary = Array(1, 2, 3)
for (elm <- ary if elm < 3) // if 之前不需有小括號括起來
  println(elm)
有了 if statement 過濾,不符合的 element,不會被 generate 出來。

當然也可在 code_block 中過濾
val ary = Array(1, 2, 3)
for (elm <- ary) {
if (elm < 3) println(elm) //在 code_block 中過濾
}
在 code_block 過濾,進入code_block的次數會比上一例的多,而且比較不好思考。
函數式的作法,通常是過濾,然後做 iteration。Scala 建議 filter 在 generator 時進行。

五. 多重過濾
若有需要多重的條件,可以使用多個 if statement,來做多重的過濾。這時的過濾條件,會變成兩個條件都成立才會 generate,也就是兩個條件做「and」運算。
val ary = Array(1, 2, 3)
for (
  elm <- ary
  if elm < 3 //第一次過濾
  if elm % 2 == 0 //第二次過濾
) println(elm)

六. 在 for expression 中宣告常數
在 for expression 中,可以宣告常數,讓程式較容易撰寫。
但 for expression 中不能宣告變數。

for (i <- 1 to 10; //這個分號是必須的,因為常數宣告與 generator 是不同的 statement
     n = i / 2 // 宣告一個常數 n,前面也可加 val
     var n1 = i / 2 // 這個 statement 是錯誤的,不能使用 var
   )
   println(n)
七. 多個 collection 的 iteration for expression 可以包含多個 generator,所以可以對多個 collection 做 iteration。 generate 的次序是,外面 collection 產生一個後,會對裡面的 collection 重新全部 generate 一遍。generate 出來的總共 element 是兩個 collection 的乘積。 兩個 generator 中間需要使用分號隔開(generator 與 generator 是不同的 statement)。
上例我們看到 generate 的次序。
val values = Array(1, 2, 3)
val types = Array("A", "B")
for (
  t <- types; //這裡的分號不能省,原因是小括號的分行不會做「分號推論」
  elm <- ary
) println("type="+ t + "elm=" + elm)
使用 filter 以及 內層 generator 參考外層 generator 的情況
for (
  i <- 1 to 5
  if i % 2 == 0; //這個分號不可少,因為小括號內不會做「分號推論」
  j <- i to 10 // 裡面那層的 generator,使用外層 generator 的資料
  if j % 3 == 0
) println(i * j)
八. for 的 generator 子句,其小括號可以使用大括號代替
val values = Array(1, 2, 3)
val types = Array("A", "B")
for { //換成大括號
  t <- types //這裡的分號可以省,原因是大括號的分行會做「分號推論」,所以原先需要分號,現在可以省略
  elm <- ary
} println("type="+ t + "elm=" + elm)
九. for 的 result value for 的完整格式為 「for (generators) yield body」, 但一般我們可以省略 yield,如前面的各個例子,都是省略 yield。 省略 yield 表示整個 for expression 的 result value 為 unit value。 若是我們希望 for expression 可以返回一串的資料,需要加上 yield。yield 會將後面 body 的 result value,收集在一個 collection 中。 for expression 結束時,這個 collection 就是 for expression 的 result value。 省略 yield,for expression 的 result 為 unit value。
使用 yield,for expression 的 result 為一個 collection。
使用 yield 加上 code_block
上例因為 println(i) 的 result 為 unit,所以產生三個 unit 的 collection。 使用 yield 加上 完整的 code_block
請注意:yield 要寫在整個的 code_block 之前,不可寫入 code_block 裡面。 yield 寫到 code_block 裡面,會導致錯誤
for expression 好東西,也是一個重要的東西,把基礎觀念弄清楚很重要。在這裡我們尚未提到 for 如何轉換成函數來執行,以後再提。 for expression 也被用在 continuation 中,雖然使用 for expression 的語法,但有點怪異,有興趣的朋友可自行查看「Scala與新的語言功能 --Continuation」。

4-4. 控制結構 --while statement

Scala 的 while 與 Java 的 while 相同。
但 Scala 沒有 for (int i = 0; i < 100; i++) 這種方式,且 Scala 建議 loop 盡量使用其他的方式來取代,比如 iteration 或 recursion。

由於 imperative 的方式通常比 functional 的方式快,所以若你的程式中某段 loop 需要較好的效能,這時使用 while loop 來製作會比較好。

while loop 有兩種格式
1. while
while (a != 0) {
  ...
}
2. do ... while
do {
  ...
} while (a != 0)

while 的 result value 為 Unit,並不是最後一個 statement 的 result value,這要特別注意。
由於 while 的 result value 為 Unit,所以有時不稱 while 為 expression。具有類似執行重複工作的 for,我們稱它為 expression,因為 for 的 result value 有很清楚的定義,我們將於下一篇中討論。

while loop 中,可能會有一個陷阱,就是 assignment 放在 while 中。
var nRead = 0
while ((nRead = in.read(buf)) != -1) { // 這裡有一個錯誤
  ...
}
nRead = in.read(buf) 是一個 assignment,所以 result value 為 unit,因此與 -1 比較一定是 false。 compiler 可以幫忙抓出這樣的錯誤,但只是一個 warning,請各位要小心。

break 與 continue 由於從某一個點直接跳離 while,會演變成程式需要仔細瞭解才可駕馭,違反函數式原則,因此以被移除。

4-3. 控制結構 --if statement

Scala 的 if statement 與 Java 的 if statement 在格式上完全相同。
但 Scala 的 if statement 有 result 值,該 result 是最後一個執行到的 statement 的 result 值。
var n = 10
if (n == 10)
  println("is ten")
else
  println("not ten")
上述程式我們可以使用 if 的 result 值重新寫
println(if (n == 10) "is ten" else "not ten")
由於 if statement 與 Java 相同,所以到此已完成本篇的目的。

請注意,Scala 沒有「三元運算子,ternary operator」,也就是「? ... :」。
int n = (a > 0) ? 10 : -10; // Scala 中沒有這樣的表示法
上例在 Java 是合法的,但 Scala 已取消這種表示法。

「為甚麼要取消?ternary operator 好用啊?!」

答案很簡單,是為了要減少 Control Structure,可以少一個是一個。

但不表示 Scala 不給替代方式,你可使用 「if (cond) true_value else false_value」這樣的方式,由於 if statement 會有 result 值,所以可以取代 ternary opeator。

上面的 Java 例子,在 Scala 可寫為
var n = if (a > 0) 10 else -10

4-2. Scala 語言精神

在討論各個控制結構之前,我們先歸納一下 Scala 語言精神,會幫助我們釐清一些疑問。

Scala 語言精神:

1. 所有的東西都是物件:你不會再看到 primitive type 的 value

2. 所有的 statement 都是 expression,都有 value:不會有沒有值的 statement

3. 除了 built-in control structures,以及宣告與 assignment 外,所有的操作都是 method call:method call 非常重要,它取代掉了許多其他語言的語法

4. 只有少數的 control structures,包含 if, while, for, try, match:請注意,這裡沒有 return 這個 keyword。可見 Scala 存心將 return 移除,大家也應盡量不要使用 return。

上述這些語言精神,深深影響 Scala 的學習,請大家努力記住。

4-1. Scala 中的控制結構(Control Structures)

之前討論過 Scala 語言的第一個部份「宣告」,我們學會:
1. class 宣告
2. object 宣告
3. 變數 /  常數 與 data field 宣告
4. method 宣告

接下來我們開始討論 Scala 語言的第二個部份「控制語法」。

Scala 承繼命令式語言的方式,所以 method 的執行是一個個 statement 照順序執行,直到遇到一些特殊情況,這個特殊情況就是我們要討論的控制結構(Control Structures)。

所謂「控制結構」是因為這些 structure 可以改變程式原有的順序式的執行方式,有控制程式執行的能力,因此稱「控制結構(Control Structures)」。

Scala 的 Control Structures 不多,只包含 if, while, for, try, match ,以及 method 呼叫。

「這些就夠了嗎?」
確實,是足夠了,雖然看起來不多,但由於 Scala 有一些特殊的語法,所以用起來一點也不會輸其他的語言。

尤其因為 Scala 可以使用 function literal(函數常數),所以我們很容易組成一個功能,而這樣的功能在其他語言可能需要引進一個新的語法,才可做到。

我們舉一個最新的例子,Java 7(撰寫本篇文章之時,Java 7 尚未正式發表) 引進一個新的語法 try-with-resource,我們可以看在 Scala 有什麼不一樣的方式。

我們先說明一下 try-with-resource 的需要原因。我們常常在程式中 open 一個 resource,然後使用它,最後把它 close。try-with-resource 讓 programmer 很方便使用 resource,程式完成後會自動 close 相對應的 resource。如下:
try (Resource resource = new Resource()) {
    use resouce...
}

這種回收 resource 的機制,在 Java 需要語法支援。在 Scala 可不需語法的支援,我們隨時可以自己增加。
由於尚有許多東西未介紹,這裡尚不仔細討論作法,未來再詳細介紹。大概的作法如下
def try_resource(resource: Resource, code_block: Function) {
   //先定義 try_resource method,以後我們呼叫本 method,達到 try_resource 的功能
   //code_block 是第二個參數,在呼叫本 method 時,傳進來一段程式碼
  try {
    code_block(resource) //將 resource 丟給 code_block 當參數,然後執行 code_block
  }
  finally {
    resource.close()
  }
}
//以下為呼叫 try_resource 的方式
try_resource(new Resource()){resource => //resource 當成本 code_block的參數
  read_resource(resource)
}
請先不要計較上例的語法,上例程式不是正確的程式, 由於有太多東西各位尚未清楚,這裡使用類虛擬碼的方式介紹。

上例呼叫 try_resource 的方式是,傳入一個 resource 當第一個參數,傳入一個 code_block 當第二個參數。
code_block 的寫法,是直接將 一段程式使用大括號括起來,這段括起來的程式碼,就可以傳進去 try_resource 當第二個參數。這種大括號括起來的一段程式碼,就是 function literal(函數常數)。

我們創造的 try_resource 與 Java 7 的 try-resource statement 用法相當類似,Java 7 需要語法支援,在 Scala 卻由我們自行創造,這其中的重要因素就是 function literal。
有了 function literal,我們可以直接將一段大括號括起的程式碼,當成參數傳給我們創造的 method,這時使用起來的感覺,很像是新的一個語法。

有了 function literal,我們可以很容易創造新的使用方法,所以雖然 Scala 的 Control Structures 比起其他語言少,但功能絕對有過之。

Scala 的語言組成份子很精簡,但容易擴充,我們若熟悉 Scala 的組成份子,未來可以針對不同的領域,設計我們的特殊「語法」,這就叫 DSL (Domain Specific Language)。

接下來章節,我們將一個個介紹 control structures,也會指出各位該注意的地方。

2011年5月3日 星期二

3-15. class 宣告總結

這篇我們總結一下 class 的宣告

1. 宣告 class:使用 class keyword
2. 宣告 singleton:使用 object keyword
2. 宣告 method:使用 def keyword
3. 宣告 data member:使用 val 與 var keyword,一個是常數,一個是變數

下一個大章節將進入 Scala 的控制結構(Control Structures)介紹,那是大家比較熟悉的東西。

3-14. Singleton 以及 Companion object / class

在 Java 我們有時會使用 static data field,或 static data method 來製作該 class 所有 instance 的共通行為。這並非好現象,因為使用到 class 共通的部份,這將破壞 Object Oriented 的完整性。

在 Java 這些共通的部份,也可能以 class singleton 的方式來表達。class singleton 的意思是該 class 只會有一個 instance,大家共通的 behavior 會以呼叫這個 single instance 的 method 來達成。

製作 singleton 程式時,通常在程式中需要宣告一個 static data field,來保存該 class 這個唯一的 instance。使用 static data field 來放置 singleton object,已幾乎成為共識。
通常我們不會在程式啟動時,就將 「singleton object」 create 出來,會延遲到程式需要該 instance 時才 create 該 instance。這導致另一個麻煩,不可重複 create instance,否則,singleton 的唯一性會被破壞,可能造成其他問題。

我們常用 singleton 來替代「共通需求」,在 Scala,更將這個概念發揮到極致。
在 Scala 每個東西都是一個 object,沒有所謂大家共通的概念,所以共通行為是不可行的。Scala 已將 static 移除,以 singleton 取而代之。S

由於 singleton 的需求眾多,為避免 programmer 沒掌握住 singleton 的 creation,因此 Scala 直接代勞。我們只需宣告 singleton object,Scala 負責幫我們處理該 object 的 creation。

在 Scala 中使用 object 這個 keyword 來宣告 singleton object,讓我們看例子
object S {
  def print(name: String) = println(name)
}
上例宣告一個 object S,請把這樣的宣告看成「在我們的系統中,有一個 object,名叫 S」。這個 object 與「使用變數宣告,然後 create instance 的 object」二者一樣。
讓我們用例子說明
object S { def print(name: String) = println(name)}
var x = "ABC"
上例,我們可以看成「程式有一個 object S,也有一個 object x」,S 與 x 沒有位階上的差異。程式可以直接呼叫 x 做工作,也可呼叫 S 做工作。
object S {
  def print(name: String) = println(name)
}
class UseS {
  def m1(name: String) = S.print(name) // 使用 S 這個 object
}
上例的 UseS 的 m1 method 呼叫 S 這個 object 做事。

技術性來講,object keyword 所做的事是定義一個 class ,並且產生與該 class 同名的一個 instance。所以 object keyword 所定義的東西與使用 class keyword 所定義出來的東西一模一樣,我們不需因為 object 這個 keyword 而有不必要的害怕或想像。

在 Java ,一個 class 可能包含 static 與 non-static 的部份。
若你將該 Java class 改寫成 Scala,原來 static 的部份,我們應該把它放入 object 的宣告,non-static 的部份我們應該把它放入 class 的宣告中。
原來的 Java class
class C {
  static void m_static(){System.out.println("static_m");}
  void m_instance(){System.out.println("instance_m");}
}
改寫成的 Scala class
object C {
  def m_static = println("static_m")
}
class C {
  def m_instance() = println("instance_m")
}

同名的 class 與 object 關係非常密切, Scala 把他們互稱為 companion(陪伴)。所以 class C 是 object C 的 companion class,object C 是 class C 的 companion object。

object keyword 所做的事,比只產生該 class 的 instance 還多
我們舉以下的例子說明
object S {
  def print(name: String) = println(name)
}
1. object keyword 主要會產生一個 companion object 的 classfile,companion object 的名稱,是原名稱後面加上$。比如上例,object S 的宣告會產生 S$ 的 classfile

2. 若程式沒宣告同名的 class(即沒有 companion class),object keyword 自動會產生 companion class 的 classfile。

companion class 具有 companion object 中的所有 method,但會變成 static method,這些 method 會 forward 給 companion object 對應的 method。

如上例,object S 會產生 S 的 classfile,且 S classfile 中有 static print 這個 method。這個 method 會 forward 給 S$ 的 print。

產生 companion class 對應的 static method,主要原因是讓 Java 程式可以使用 companion class 的 static method 呼叫到 Scala 中 singleton object 的 method,這樣一來 singleton object 扮演 static 角色的感覺就更加強烈。

範例:只有 object,沒有 companion class時

可以清楚看到 object S2,產生S2$.class與S2.class。S2$.class 是 object S的本體。S2.class 將 S2$.class 的 method 放置一份過去,並且變為 static。














範例:有 object,也有 companion class時

class S2 產生 S2.class。
object S2 產生 S2$.class,且將一些 method 放入 S2.class。


我們仔細觀看 S2.class,S2有 m1 method,這是 S2 本身定義的。S2也有 sayHello,這是 object S2定義的 method。

在本例中 S2.class 的 m1 method,呼叫 object S2 的 syaHello method,我們來看如何呼叫

在 m1 method 中,可以很清楚看到呼叫 S2$.sayHello,印證 object S2 被編譯成 S2$。

對於 singleton 我們還有一點需要特別加以說明,在 OO 裡面,static method 是不能被 override 的,這個你應該要認識,因為 override 是 instance 才會有的動作,在 static level 做 override 是沒有意義的。有些語言,甚至將 static method 編譯為 final,表示不能再被 override。

Java 的 static method 也是同樣的情況,不能被 override 的。
若你在 subclass 宣告一個與 parent class 相同名稱的 static method,Java 允許你這樣做,可是其實是危險的。因為 Java compiler 其實認定你在 subclass 重新宣告了一個新的 method,只是名稱與 parent class 的相同,所以效果並不是 override。下例中,我們使用 @Override 來驗證這個推論。
範例:
class J {
  static void m1() {
    System.out.println("J.m1");
  }
  static void m2() {
    System.out.println("J.m2");
  }
}
class J11 extends J1 {
  static void m1() { // 注意本 method 並非 override J.m1,真正的效果與定義一個新的 method 完全相同
    System.out.println("J11.m1");
  }
  @Override //我們使用 @Override 來宣告要 override J.m2,這裡會產生 compile error。
  static void m2() {
    System.out.println("J11.m2");
  }
}
Singleton object 主要是處理原來 Java 所需要的 static 的部份,以剛剛的論述我們知道 static 的部份不應該加以 override,或者說 static 的部份做繼承是沒有意義的動作。基於這個理由,Scala 的 singleton object,也是不能繼承的,這件事你應該樣記住。
object S
class S1 extends S//class 繼承 object,錯誤!!
object S
object S1 extends S//object 繼承 object,錯誤!!
注意:但 object 可以繼承 class。這是因為 object 取的是 parent class 的 instance 部份,當然允許繼承
class S
object S1 extends S//object 繼承 class,這是很正確的作法!!

3-13. 大括號省略原則

Scala 中我們常需要使用大括號將某一些東西刮起來。比如,一段程式碼,我們需要使用大括號刮起來。又比如,class 宣告我們也需要使用大括號刮起來。

但若一段程式碼,只有一個 statement,此時我們可以將大括號省略。
若 class 宣告中,並沒有其他的程式片段,我們也可以將大括號省略。
這個就叫大括號省略原則。
def p = {
  println("abc") //大括號內只有一個 statement 的 code block,大括號可以省略
}
def p = println("abc") // 省略大括號
以上兩個 statement 是等義的。
class S { // class 宣告,但裡面沒東西,可以省略大括號
}
class S // 省略大括號
以上兩個 statement 是等義的。

3-12. method 宣告

method 宣告,Java 使用的格式是
  type method_name(param1, param2, ...) {
    ...
  }

1. 參數的宣告方式與變數宣告方式相同,格式為 type var_name。
2. Java 使用 return statement 來結束 method 並傳回 return 值。若宣告 void 的 method,不可有 return statement。

請看實際程式案例
class J1 {
  String sayHello(String name) {
    return "Hello " + name;
  }
}

在 Scala 中宣告 method 的方式為:
  def method_name(param1, param2, ...): type = {
    ...
  }

關於 Scala 宣告 method 的重點如下:
1. 使用 def 說明要宣告一個 method
2. 冒號後的 type 是 method 的 return type
3. 可省略 return type,此時 compiler 將會使用 type inference,推論 method 的 return type
4. 若 method 使用到 recursion,method 的 return type 不可使用省略(即,return type inference 不能使用在 recursive method 中)
5. 若使用 return statement 來傳回值,return type 不能推論,因此 return type 不可省略
6. method 宣告後的等號表示「把後面的 code block 設定給本 method」,函數宣告使用等號在函數式語言中非常常見,表明在該函數等於後面的 code block
7. method 後面的等號通常不能省略,但為維持與 Java 類似的格式,所以有時省略。當省略等號時視同宣告該 method 的 return type 為 Unit(沒有 return 值)。
8. 省略等號通常容易導致誤會,不是好習慣,建議等號永遠都不要省略。
9. method 一般使用最後執行到的 statement 作為本 method 最後的傳回值。但也可使用 return 作為 method 結束並傳回值,但並不建議使用 return statement 結束 method 並傳回值。
10. 參數宣告與變數宣告方式相同,格式 var_name: type。method 中參數的 type 一定要有,參數無法使用 type inference。
11. 若 code block 只有一個 statement,可以省略大括號 { 與 },這樣看起來會比較像函數宣告
例:
class S1 {
  def sayHello(name: String): String =  {
    "Hello " + name
  }
}

範例:code block 只有一個 statement時,可以省略大括號 { 與 }
class S1 {
  def sayHello(name: String): String =  "Hello " + name //省略大括號
}
範例:return type 推論
class S1 {
  def sayHello(name: String) =  { //省略 return type,compiler自行推論,本例推論出來為 return String
    "Hello " + name
  }
}
範例:使用 recursive,不可推論 return type
class S1 {
  def sayHello(name: String) = sayHello("Hello " + name)
                  //省略 return type,想要做 return type inference,但出錯
}


範例:參數需指明 type,不可使用 type inference
class S1 {
  def sayHello(name) = Hello " + name //省略參數 type,想要做 type inference,但出錯
}

範例:使用 return statement,需要宣告 return type
class S1 {
  def sayHello(name: String) = {
    return Hello " + name // 使用 return statement,method 的 return type 不可省略
  }
}


範例:省略等號,視同宣告 method 的 return type 為 Unit
class S1 {
  def sayHello(name: String) {//省略等號,視同宣告 return type 為 Unit,雖使用 return statement,來 return String,但 method 的 return type 仍然為 Unit
    return Hello " + name 
  }
}
上例是熟 Java 者常犯的錯誤,因為熟 Java 者常忘記 method 後面等號,又以為會使用 return type inference,所以以為上例的 return type 為 String,其實真正為 Unit。上例並不會引發 compiler 錯誤。
範例:省略等號,視同宣告 method 的 return type 為 Unit,但若又宣告 return type
class S1 {
  def sayHello(name: String): String {//省略等號,視同宣告 return type 為 Unit,但又宣告 return type,引發錯誤
    return Hello " + name 
  }
}


宣告 method 請依照下列 guideline 比較不會有問題:
1. 一定要使用等號,使用等號是好習慣
2. 參數要有 type
3. recursive method 需要宣告 type
4. 不要使用 return statement 來結束執行並傳回值,使用最後一個 statement 是 method 傳回值的原則

2011年5月2日 星期一

3-11. 驗證 assignment 與 method 的 result 值

之前提到,每個 statement 都有 result 值,既是如此,我們可以將任何一個 statement 指定給一個變數,或是印出該 statement 的 result 值。

以下的命令在 Scala 是合法的
var v = println("a")  // 將 println 的 return 值,指定給 v。v 此時的值是 println("a") 的 return 值,應該是 unit value
println(v = 10) // 將 v = 10 的 result 值,丟給 println 印出。傳進 println 的值為,assignment 後的值,所以應該是 Unit

第二個 statement,有一些小問題,各位應該有以下的疑問
「由 statement1, 可以推論出 v 的型態應該是 Unit,那 statement2, v = 10,將 10 設定給 Unit type 的變數 v,這是可以嗎?」

答案是,變數的型態為 Unit 時,可以接受 assignment,但值不會改變(可設定,但值沒改)。

我們看執行範例
第三個 statement 直接設定 v 的值為 100,但 v 仍是 unit value,沒有改變。

2011年4月29日 星期五

3-10. method 的 return 值,以及 break/continue

在 Java 我們在 method 的定義中,要 return 一個值,需要使用 return 這個 keyword。

在 Scala,除可使用 return keyword 來作為 method 的 return 值外,也可使用 method 最後一個執行到的 statement 作為整個 method 的 return 值。這樣的方式,在許多的語言中出現。

Scala 鼓勵使用最後一個執行 statement 的值為 return 值,而不鼓勵使用 return。不鼓勵的原因在於 Scala 引進函數式的觀念。

在函數式的語言中,對於破壞整個流程的結構(construct)非常不歡迎,因為這隱含你需要一個一個 statement 循序走下去,才知道何時跳開原有流程。

我們習慣在程式中,把某一變數與某個值比較,再視不同的值走不同的程式路徑。這種方式,函數式語言是不歡迎的,因為整個函數邏輯被分割成多塊。而 return 這種忽然由某一個 statement 中直接跳出 method 的作法,是函數式語言不能忍受的,因此 Scala建議你不要使用這種方式。

同樣的原因,break 與 continue 這兩個在 Java loop 中常用到的 statement,也完全不符合函數式語言的概念。
Scala 建議大家盡量少用 loop 來製作程式(你應該盡量使用 iteration 與 recursion來取代 loop), break 與 continue 這兩個突兀式脫離迴圈的方式,會加深 loop 誤用的程度,是 Scala 所要改革的對,因此在 Scala 中已遭移除,你應該注意。

既然移除 break / continue,以相同的邏輯,Scala 應該一併把 return 移除,但 Scala 只是建議你不要使用,可見 return 的影響力。相信 Martin 的團隊針對 return 是否移除,一定有過相當多的激辯。

3-9. Assignment的 result 值

之前說到,在 Scala 中每個 statement 都有 result 值,這也包含 assignment。
但請注意,Scala中,assignment 的 result 型態是 Unit型態,result value 為 unit value,並非是右值(right-hand-side value),這與一般人的想像不同,請小心。

請看下例


第一個指令宣告變數 n。
第二個指令,使用 assignment 設定 n=20,並印出 result 值。由 output 可以看到 result 值為 (),是Unit type。

我們可能犯以下的錯誤,連續的 variable assignment,誤以為值會傳遞過來。如:
n1 = n2 = 100
上面的指令,我們可能會以為n1, n2 的值都會是 100,其實不是,n1 會被設定為 ()。

我們看實際的案例

3-8. Unit 型態

Uni t型態是一個特殊的型態,起源於 tuple(多個值的型態)。

tuple 是一個重要的東西,我們未來會詳加介紹。這裡簡單說明,tuple 使用簡單的小括號將各個值放在一起,是一種非常有用的資料結構。

A. 一個資料的tuple,如 (1) *附註
B. 兩個資料的tuple,如 (1, "A")
C. (1, 2, "A", 5.0) 是一個 4 值的 tuple。

tuple 可用來放多個值,但 tuple-0 表示什麼?
tuple-0 只可有一個值,就是空的 tuple,即 ()。

這個 tuple-0 的型態在 Scala 把它特別表明出來,稱為 Unit 型態。Unit型態,只有一個instance,稱為 unit value,即 ()。

為何需要特別標明出來呢?原因是要用他來表明特別的東西。

之前提到,所有的 statement 都需要有 result 值,這包含 method,但若是 method 真的沒有 return 值呢?這時就把它表明為 return 回「空資料的 tuple」,這就是 Unit。
因此如果 method 沒有 return 值,在 Scala 的意義就是該 method 的 return type為 Unit,return 值為()。

當 Java 與 Scala 混用時, Java 中 void 型態的 method,在 Scala 會變成該 method return type 為 Unit。

附註:因為一個資料的 tuple,小括號會被解釋運算,所以 (1) 會被視為 1 這個 Int,而非 Tuple (Int),若確定要使用一個值的 Tuple,需要使用 Tuple1(1)

2011年4月24日 星期日

3-7. statement 的 result 值

在不同的程式語言,對於 statement 的 result 值定義不太相同。有些語言的某些 statement 可以沒有 result 值,比如 while loop 可能就沒有 result 值。

「那在 Scala 中的情況呢?」

Scala 的情況是,「所有的 statement 或 expression 都有 result 值」,沒有任何例外,包含不需 return 回任何東西的 method,也都有 result 值。

各個 statement 的 result 值有相關的規定,這包含 method 呼叫與 assignment。我們後續會特別針對 method 與 assignment 做討論。

3-6. 型態推論(Type Inference)

型態推論是一個很重要的概念,可以讓 programmer 大幅減少程式的撰寫,就如同使用動態型態的方便性。

型態推論可不是Scala所發明,早在以前,一些函數式語言與一些命令式語言都已經出現型態推論。千萬不要以為 Scala 這麼神勇,什麼東西都開創新局,其實它只是參考很多好的觀念,把它加到自己的語言設計中罷了。

支援Type Inference的語言包含(參考wiki http://en.wikipedia.org/wiki/Type_inference
1. Visual Basic:9.0開始
2. C#:3.0開始
3. Clean
4. Haskell
5. ML
6. OCaml
7. Scala
很多吧!這也突顯出Type Inference的重要性。

可還記得之前的討論,為了要加速開發,以及爭取 Script 的使用者,Scala 想要能夠有 Dynamic Type 的便利性,以及 Static Type 的嚴謹度(嚴謹可以讓 compiler 幫你多做一點事,及早檢查出程式的問題,增加程式的品質),這時型態推論就派上用場。

所謂型態推論是 compiler 可以幫忙推論出型態的地方,你就可以省略指明型態。

「這是什麼意思?」

讓我們回到宣告變數的範例:
var n = 10
right hand side 的值10,我們很清楚 10 是一個 Int 的型態,由於「不同型態的兩個變數或值是不可互相指定的」,所以我們知道上述 statement 的 n 應該是 Int 型態(當然也有可能是 Int 的 parent class)。這樣的推論,對 compiler 也不會有太大的困難,很容易幫我們代勞。

如果我們的想法是「現在宣告的變數,與右值(right-hand-side)的型態相同」,我們就可以大膽讓 compiler 幫我們代勞,推論出該變數應該具有的型態。這樣的推論代勞就叫型態推論(Type Inference)。

這種方式是不是很方便,是否有點像使用 Dynamic Type 語言的作法!

其實,type inference 更有用的地方在於 function(或稱method)的 return 型態。

在 Java,每個 method 都需要有 return 的型態,就算沒有 return 型態,你也要指明它是 void。void 可以把它看成是一種特殊的型態,表示不 return 值。

在 Scala,method 一定有 return 型態的堅持仍在,Scala 中每個 method 都需要有 return 型態。但我們在 Scala 中宣告 method,卻通常省略宣告 method 的 return 型態,讓我們的程式看起來更簡潔易懂。我們會希望 compiler 使用型態推論幫忙找出 method 的 return 型態。

method 我們尚未討論,這裡我們先舉個小例,說明型態推論在 method 中的情況。
def f1(n: Int) = {
  n + 100
}
上例我們定義一個 method f1,我們沒有指明 return type,但 compiler 會幫我們找出來。

Scala 使用 method 最後一個執行到的 statement 的值作為整個 method 的 return 值。f1 的 return 值很清楚是 n + 100。因為 n + 100 的型態為 Int,所以 f1 的 return 型態推論出是 Int。

型態推論雖然方便,但注意不要任意延伸!

「嗯,那我瞭解了,型態推論很方便,所以 method 的參數也可以推論吧?」
def f1(n) = {n+100}
因為 n + 100,所以 n 唯一的可能型態是 Int,compiler可以幫我自動推論 n 的型態,這樣又可以少打幾個字。

以上就是任意延伸的案例,雖然上述的推論看起來沒有問題(其實還是有問題,n 可能是其他型態,但具有 + 的 method),但在 Scala 中參數是不可以被推論的!method 的一定要明確說明型態,否則 compiler 會有 error。

關於型態推論,結論是
1. 變數可以推論
2. method 的 return type 可以推論
3. method 參數不可推論

參數型態推論是一個容易犯的錯誤,另一個典型的型態推論錯誤延伸是「型態轉換」,我們先看例子
var n = 10
n = 10.5
第二個 statement 會 compile 錯誤!

「奇怪,不是會推論嗎? n 在第二個 statement 變成 Float 就好了啊?」

錯誤的原因不是推論的問題,而是搞錯靜態語言的特性。
所謂靜態語言是變數的型態一旦決定後,就不會改變的,這叫靜態語言。

以靜態語言而言,第二個 statement 的意思是要把一個浮點數的值,設定給一個 Int 型態的變數,你說,此時 compiler 會如何做?當然就是告訴你 type mismatch。

請大家不要混淆了 variable initialize 與 variable value assignment。
1. Variable initialize 發生在變數宣告時,Scala 要求變數宣告時需要設定初值,變數宣告時,也需要決定該變數的型態,因此可以使用型態推論。
2.Variable value assignment 是變數宣告之後,重新設定新值,因為變數早在宣告時就已固定型態,這時沒有所謂的型態推論。使用不同的型態設定給變數,隱含型態轉變,compiler 自然不會允許。


「奇怪?可是很多 Script 語言中可以這樣做啊?」

沒錯,比如在 JavaScript 中你確實是可以這樣做。
原因是在於這些 Script 語言使用動態型別,而且有些還支援動態轉換(如把 String 轉成對應的數值資料),所以當你執行第二個 statement 時,script engine會以為你要重新設定 n 的型態,可以馬上幫你轉換。

Scala 是靜態型別的語言,不允許你重新設定變數的型態,當你要將 10.5 指定給 n 時,會以為你型態搞錯了,自然給你一個錯誤。

型態推論雖然好用,但不能有過度的期待,Scala 還是堅守 Static Type 的界線,自動轉換(比如型別的轉換,如 Int 轉成 String,或是 String 轉成 Int)不是可以使用型態推論做到的。

若需要型態的轉換(比如讓用戶輸入一個數值的字串,程式要由字串轉成對應的值),此時仍然需要 explicit 指出,或是使用未來介紹的 implicit conversion 機制才可解決。

2011年4月23日 星期六

3-5. 常數 / 變數 與 mutable / Immutable

之前我們曾提及 val 與 var,val 大約可比擬其他語言的 const。
val 在 Scala 非常被強調,盡量可以使用 val 的地方,請盡量使用。

接觸過函數式語言的洗禮後,你應該知道 Scala 是不喜歡變數這種東西的,因為不管是為了支援 concurrency,或是降低副作用(Side-Effect),使用變數都不是一種好習慣。

這個原則在你平常的程式撰寫也是同樣,就算你不使用 Scala,也應謹守「盡量不使用變數」的原則。「用修改變數值來作為控制程式的手段」這個習慣真的不好,儘快改變掉這習慣,對你在學習 Scala,或撰寫好的程式都會有很大的幫助。

除了 val 外,還有一個看起來很類似的東西,Immutable Data Type。

所謂 Immutable Data Type,就是這個 Data Type裡面的 field不能被更改。Mutable的英文字是可更改的,Immutable 的意思就是不可更改的。
所謂 Immutable 就是不能被更改,表示一旦設定值(其實應該叫被 initial 值之後),以後不能被改變。

「這個 Immutable 好像和 val 一樣啊?為何有了 val 又要有 Immutable?」

不,val 與 Immutable 是不相同的東西。

我們首先釐清一個觀念,在 Java 中所有的 object (或 Scala 中,大部分的 object)都是 reference type。reference type 的意思是,變數只儲存一個 reference 到真正儲存資料的記憶體,變數並非擁有真正的資料。我們姑且可以把 reference type 當成一個 pointer,也就是, Java 的 object 變數,只是一個 pointer,point到真正的資料。

val 指的是「該變數(或叫該常數好了)不能再被變動」。所以 val 的變數不能被重新 assign 新的值,但並沒有限定被指定到的地方不能該新值。

讓我們舉例說明
class People(name: String) // 宣告一個 class 叫 People,有一個 name 的 data field。現在可能暫時有點看不懂,請先跳過一下
val p1 = new People("John") // 產生一個 People instance,他的 name 叫 John,注意 p1 是 val (常數)
p1.name = "Jason" // 修改 p1.name,這是正確的,雖然 p1 是 val
p1 = new People("Jason") // 重新 assign 一個 object 給 p1,但由於 p1 是 val,所以 compiler 出現錯誤

第三個 statement 可以正常工作,你看出問題了嗎?
沒錯,雖然 p1 是 val,但 p1 可以變動 name 欄位。若有兩個 thread,一個讀取 p1.name,一個更改 p1.name,不是有可能產生 race condition 嗎?

確實沒錯,所以才會有 Immutable Data Type 這個東西, Immutable Data Type 表示該值不能該動,也就是不希望透過變數更改到欄位值。

「有沒有實例呢?」
有,其實你也用過, Java裡面的 String,就是一個 Immutable 的 data type。讓我們看例子
var s1 = "A" //設定 s1 為 "A" 字串
var s2 = s1 //設定 s2 指到 s1,s1, s2 此時都指到 "A" 字串
s1 = "B" //設定 s1 為 "B" 字串
println(s2) // 這裡會印出 "A"
上述的 statements 是正確的。
最後 statemenet 要印出 s2,結果會是"A"。
原因在於第三個 statement,雖然更改了 s1 的指向指到 "B",但此時 s2 並沒有改變,仍舊指到舊的 "A",所以印出 "A"。
String 這個 class 的 instance 一旦被 create 之後,instance 裡面的值就不可被改變,雖然 s1 更改了,但此時系統會重新 create 一個新的 String instance 給 s1,此時 s2 所指的 instance 完全沒有更改。

到此,我們應該很清楚
1. val 所訂定的是變數本身,說明的是,該變數不能被 reassign。
2. Immutable 指的是,所指到的資料結構不能被更改。
val 與 immutable 兩者所訂定的對象不同。若要完整支援 concurrency,val 配上 immutable 是最合適的。

「除 String 外,還有什麼東西是 Immutable?」

其實,Scala 幫大家準備好了。我們常用的資料結構,List、Set、Map,在 Scala 世界都有兩份,一份是 mutable,另一份是 immutable,供大家使用。

使用 mutable 的版本,就和在 Java 的使用情況相同,允許我們在同一個 instance 中增加、修改、或減少該 instance 的資料。使用這種版本,在 multi-thread 的情況下,programmer 需要自行掌握資料的完整性 (data consistence)。

使用 immutable 的版本,instance 一旦 initial 完畢,該 instance 的資料就不會再被改變。如果呼叫該 instance 的 add 或 delete 等修改資料的 method,系統會 create 一個新的 instance,並把原來 instance 資料 copy 一份過來,然後做修改,最後返回這個新的 instance。

「create 一個新的 instance,並把原來 instance 資料 copy 一份過來,然後做修改」,這幾個動作在新的 instance 初始資料時一次完成。當修改資料的 method 完成時,所使用到的資料結構已經是完全新的,舊的資料結構完全沒有被更動到。

舉例說明,若有兩個變數 x1, x2 指到同一個 immutable 資料 d1,其中變數 x2 呼叫修改資料 d1 的 method m(),此時系統的執行狀況如下
1. 產生一個新的 immutable 資料 d2,d2 的初始值使用 d1,以及 method m()
2. 讓 x2 指到新的資料 d2
3. d1 沒有任何改變,x1 也沒有任何改變,仍然是指到 d1

由上述的動作,我們可以瞭解 x1 並沒有變化,x2 的改變只改變到它自己的部份。所以就算 x1 與 x2 在不同的 thread,彼此也不會互相影響,不會有 race condition 的情況發生,這對撰寫concurrency 程式是一大保障。

Immutable 的資料結構帶來程式穩定的保證,但它的代價是「當修改的 method 呼叫時,重新複製一份資料的記憶體浪費,以及複製的時間」。

Mutable 與 immutable 這兩種版本, Scala 的 default 使用 immutable 版本。請不用擔心無法使用 mutable 版本,Scala 這裡的 default 表示你不特別加以指定時會使用 immutable。你隨時可以使用 mutable 版本,甚至在同一個 class 中使用兩個不同版本的資料結構,一個是 mutable 另一個是immutable。

val 在 Scala 中非常被強調, immutable 也被同樣強調,主要原因是要讓開發 concurrency 程式不再是那麼痛苦的事。