[譯] Scala 型別的型別(二)

ScalaCool發表於2017-04-21

本文由 Yison 發表在 ScalaCool 團隊部落格。

上一篇

Scala 型別的型別(一)

目錄

6. 一個單例物件的型別

Scala 的單例物件( object) 是通過 class 實現的(顯然後者就像 JVM 的基礎構件)。然而你也會發現我們並不能像一個簡單的類一樣,輕鬆地獲得一個單例物件的型別……

我常常疑惑該如何傳一個單例物件給一個方法,對此我自己也非常驚訝。我的意思是指 obj: ExampleObj 是無效的,因為這種情況 ExampleObj 已經指向了例項,所以它有個 type 的成員,我們可以靠它解決問題。

下面的程式碼解釋了大概的方法:

object ExampleObj

def takeAnObject(obj: ExampleObj.type) = {}

takeAnObject(ExampleObj)複製程式碼

7. Scala 中的型變

術語 翻譯
Variance 型變
Invariant 不變
Covariant 協變
Contravariant 逆變
Immutable 不可變的
Mutable 可變的

上述表格由譯者自主新增,避免造成誤解。

型變,通常可以解釋成型別之間依靠彼此的「相容性」,形成一種繼承的關係。最常見的例子就是當你要處理「容器」或「函式」的時候,有時就必須要處理型變(極其的常見!)。

Scala 跟 Java 一個重大的差異,就是它的「容器型別」預設是不變的!也就是說,如果你有一個定義為 Box[A] 的容器,然後在使用的時候將其中的型別引數 A 替換成 Fruit,之後你就不能插入一個 Apple 型別(Fruit 子類)的值。

Scala 中的型變通過在「型別引數」前使用 +- 符號來定義。

參見:www.slideshare.net/dgalichet/d…

概念 描述 Scala 語法
不變 C[T'] 與 C[T] 是不相干的 C[T]
協變 C[T'] 是 C[T] 的子類 C[+T]
逆變 C[T] 是 C[T'] 的子類 C [-T]

以上的表格較抽象地羅列了所有我們需要擔心的型變情況。也許你還在疑惑什麼時候需要關心這些,事實上當你每次處理 collection 的時候就遇到了 — 你必須思考「這是一個協變嗎?」。

大部分不可變的 collection 是協變的,而大多數可變的 collection 是不變的。

在 Scala 中至少有兩個不錯並很直觀的例子。一個是 collection,我們將使用 List[+A] 來舉例;另一個就是「函式」。

當我們討論 Scala 中的 List 時,通常指的是 scala.collection.immutable.List[+A] ,它是不可變的,且是協變的。讓我們看看這與「構建一個包含不同型別成員的 list」有什麼聯絡。

class Fruit
case class Apple() extends Fruit
case class Orange() extends Fruit

val l1: List[Apple] = Apple() :: Nil
val l2: List[Fruit] = Orange() :: l1

// and also, it's safe to prepend with "anything",
// as we're building a new list - not modifying the previous instance

val l3: List[AnyRef] = "" :: l2複製程式碼

值得一提的是,當存在不可變的 collection 時,協變是安全的。如果 collection 可變,則不成立。這裡典型的例子是 Array[T],它是不變的。下面就來看看「不變」對我們來說意味著什麼,以及它是如何讓我們免於錯誤:

// won't compile
val a: Array[Any] = Array[Int](1, 2, 3)複製程式碼

因為 Array 的不變,這樣一個賦值操作就不會被編譯。假使這個賦值被通過了,我們就陷入麻煩了。我們會寫出這樣子的程式碼:a(0) = "" // ArrayStoreException!,這將引發可怕的 ArrayStoreException 失敗。

我們曾說過在 Scala 中「大部分」不可變的 collection 是協變的。如果你想知道一個「相反是不變」的特例,它是 Set[A]

7.1 特質(trait)— 可以帶有實現的介面

首先,讓我們看看關於「特質」最簡單的一個問題:我們如何將多個特質混入到一個型別中,就像如果你來自 Java,會把這叫做「介面實現」一樣:

class Base { def b = "" }
trait Cool { def c = "" }
trait Awesome { def a ="" }

class BA extends Base with Awesome
class BC extends Base with Cool

// as you might expect, you can upcast these instances into any of the traits they've mixed-in
val ba: BA = new BA
val bc: Base with Cool = new BC

val b1: Base = ba
val b2: Base = bc

ba.a
bc.c
b1.b複製程式碼

目前而言,你應該都比較好理解。現在讓我們來討論下「鑽石問題」,熟悉 C++ 的讀者可能一直在期待吧。鑽石問題(菱形繼承問題)主要描述的是在「多重繼承」的情況下,我們「無法明確想要繼承什麼」的處境。如果你認為特質也類似多重繼承一樣,下圖揭示了這個問題。

7.2 型別線性化 VS 鑽石問題


[譯] Scala 型別的型別(二)
Diamond Inheritance

要說明「鑽石問題」,我們只要有一個 BC 中的覆蓋實現就行了。當我們呼叫 D 中的 common 方法的時候,產生了歧義 — 我們到底是繼承了 B 還是 C 的方法?在 Scala 裡,如果僅僅只有一個覆蓋方法的情況下,這個問題很簡單 — 就是這個覆蓋方法。但假使是更復雜的情況呢?讓我們來研究一下:

  • class A 定義了方法 common ,返回 a
  • trait B 覆蓋 common ,返回 b
  • trait C 覆蓋 common ,返回 c
  • class D 同時繼承 BC ;
  • 請問 D 繼承了誰的 common ?到底是 C ,還是 B

這種歧義是每個「多重繼承」機制的痛點之一,Scala 通過一種稱為「型別線性化」的手段來解決這個問題。
換句話說,在一個鑽石類結構中,我們總是可以明確地決定在 D 中要呼叫的 common 方法。我們先來看看下面這段程式碼,再來討論線性化:

trait A { def common = "A" }

trait B extends A { override def common = "B" }
trait C extends A { override def common = "C" }

class D1 extends B with C
class D2 extends C with B複製程式碼

結果如下:

(new D1).common == "C"

(new D2).common == "B"複製程式碼

之所以會這樣,是由於 Scala 在這裡為我們採用了型別線性化規則。演算法如下:

  • 首先構建一個型別列表,第一個元素就是我們首要線性化的型別(譯者注:剛開始列表是空的);
  • 將每個超型別遞迴地展開,然後把所有的型別放入到此列表中(這應該是扁平的,而不是巢狀的);
  • 刪除結果列表的重複項,從左到右對列表進行掃描,刪除已經存在的型別;
  • 操作完成。

讓我們將這個演算法人肉地應用到我們的鑽石例項當中,來驗證為什麼 D1 extends B with C(以及 D2 extends C with B
會產生那樣的結果:

// start with D1:
B with C with <D1>

// expand all the types until you rach Any for all of them:
(Any with AnyRef with A with B) with (Any with AnyRef with A with C) with <D1>

// remove duplicates by removing "already seen" types, when moving left-to-right:
(Any with AnyRef with A with B) with (                            C) with <D1>

// write the resulting type nicely:
Any with AnyRef with A with B with C with <D1>複製程式碼

顯然,當我們呼叫 common 方法時,可以很容易決定我們想要呼叫的版本:我們只需看一下線性化的型別,並嘗試從右邊的線性化型別結果中解析出來。在 D1 的例子中,實現 common 的特質是 C,所以它覆蓋了 B 提供的實現。在 D1 中呼叫 common 的結果將是 "c"

你可以認真考慮在 D2 上嘗試這種方法 — 如果你執行程式碼,它應該會先後對 CB 進行線性化,從而產生一個為 "b" 的結果。並且,你也可以簡單地利用「最右取勝」的原則來簡化線性化規則的理解,但儘管這個有用,卻並沒有展現整個演算法的全貌。

值得一提的是,我們也可以通過這種技術來獲知「誰是我們的超類?」。如同線上性化型別中「朝左看」一樣簡單,你就能知道任何類的超類是誰。所以在我們的 D1 例子中,C 的超類是 B

8. Refined Types (refinements)

Refinements 可以很簡單地理解為「匿名的子類化」。所以在原始碼中,可以是類似這個樣子:

class Entity

trait Persister {
  def doPersist(e: Entity) = {
    e.persistForReal()
  }
}

// our refined instance (and type):
val refinedMockPersister = new Persister {
  override def doPersist(e: Entity) = ()
}複製程式碼

9. 包物件

Scala 在 2.8 版本中引入了包物件(Package Object),這本身並沒有真的擴充了型別系統。但包物件們提供了一種相當有用的模式,可以一起引入一堆東西,此外編譯器也會在它們那尋找隱式的值。

宣告一個包物件很簡單,只要一起使用 packageobject 關鍵字就行了,就像這樣子:

// src/main/scala/com/garden/apples/package.scala

package com.garden

package object apples extends RedApples with GreenApples {
  val redApples = List(red1, red2)
  val greenApples = List(green1, green2)
}

trait RedApples {
  val red1, red2 = "red"
}

trait GreenApples {
  val green1, green2 = "green"
}複製程式碼

約定上,我們將包物件們定義在 package.scala 中,然後放置到目標 package 下。你可以通過調查上述例子的檔案源路徑以及 package 來加深理解。

從使用方面來說,這帶來了真正的好處。因為當你引入包的時候,你也隨之引入了在包中定義的所有狀態:

import com.garden.apples._

redApples foreach println複製程式碼

10. 型別別名

型別別名(Type Alias)並不是另一種型別,而是一種我們提高程式碼可讀性的技巧。

type User = String
type Age = Int

val data:  Map[User, Age] =  Map.empty複製程式碼

通過這樣的技巧,Map 的定義一下子變得很清晰。如果我們僅僅只使用一個 Sting => Int 的 map,程式碼的可讀性就不那麼好了。雖然我們仍舊可以堅持使用我們的原始型別(也許是出於如效能方面的考慮),但使用別名能讓這個類後續的讀者更容易理解。

注意,當你要為一個類建立別名的時候,並不會為它的伴生物件也建立別名。舉個例子,假使你定義了 case class Person(name: String) 以及一個別名 type User = Person,呼叫 User("John") 就會出錯。因為 Person 的伴生物件並沒有別名,就不能如預期般有效呼叫 Person("John"),後者會隱式地觸發伴生物件中的 apply 方法。

相關文章