本文由 Yison 發表在 ScalaCool 團隊部落格。
上一篇
目錄
11. 抽象型別成員
現在我們來深入瞭解「型別別名」的使用場景 — 抽象型別成員(Abstract Type Member)。
有了抽象型別成員,我們就可以說「我希望有人告訴我某個型別 — 我將通過名稱 MyType 來引用它」。抽象型別成員最基本的功能就是讓我們能夠定義泛型類(模板),但卻不是通過使用 class Clazz[A, B]
這種語法,而是在類裡面進行命名,就像這樣子:
trait SimplestContainer {
type A // Abstract Type Member def value: A
}複製程式碼
學過 Java 的朋友會覺得這在語法上有點類似 Container<
,但在 「路徑依賴型別」 的章節中我們會發現它其實是更強大的,以下的例子也可以說明這一點。
A>
需要注意到的關鍵點是,雖然 Abstract Member Type 命名中包含了關鍵字 abstract
,它並不會表現得跟一個「抽象欄位」一樣 — 所以你仍然可以在不「實現」型別成員 A
的前提下建立一個 SimplestContainer
的新例項:
new SimplestContainer // valid, but A is "anything"複製程式碼
你可能會想知道型別 A
到底是什麼,因為我們沒有在任何地方提供關於它的任何資訊。然而實際上 type A
無非只是 type A >
的一個縮寫而已,它可以代表「任何東西」。
: Nothing <
: Any
object IntContainer extends SimplestContainer {
type A = Int def value = 42
}複製程式碼
我們通過使用型別別名「提供了一個型別」,現在我們可以實現這個 value
方法,返回一個 Int
。
我們可以對「抽象型別成員」進行約束,這是它更加有趣的應用。假設你想要一個容器,只能儲存一個 Number
的任何示例。我們可以在定義一個抽象型別成員的地方註釋以下的約束:
trait OnlyNumbersContainer {
type A <
: Number def value: A
}複製程式碼
或者我們可以稍後在類的繼承關係中新增約束,比如繼承一個宣告「only Numbers」的特質:
trait SimpleContainer {
type A def value: A
}trait OnlyNumbers {
type A <
: Number
}val ints = new SimpleContainer with OnlyNumbers {
def value = 12
}// bellow won't compileval _ = new SimpleContainer with OnlyNumbers {
def value = "" // error: type mismatch;
found: String("");
required: this.A
}複製程式碼
因此,就如你看到的,我們可以像使用「型別變數」一樣使用「抽象型別成員」,但是卻不必像前者一樣到處顯式傳遞,因為它不是一個欄位。雖然這裡需要付出一點代價 — 我們需要給這些型別取名字。
12. 自遞迴型別
自遞迴型別(Self-recursive Types)在大多數文獻中被稱為 F-Bounded Types 。所以你可能會發現很多文章或部落格引用 F-bounded 。事實上,這是 self-recursive 的另一種叫法,代表了「子型別約束」本身是通過引數化發生在左側的繫結器的情況。
由於「自遞迴」的叫法更加直觀,我們會在後續的文中堅持使用(儘管還是有部分讀者會在 google 中搜尋「F-bounded是什麼」)。
12.1 F-Bounded Type
雖然這不是 Scala 的某種具體型別,但它有時也讓人感到棘手。很多人熟悉(也許是不知不覺地)的一個自遞迴型別的例子是 Java 中的 Enum<
。如果你比較好奇,可以參見 Enum sources from Java 。但現在先讓我們回到 Scala,看看我們到底在討論什麼。
E>
在本節中,我們不會特別深入探討這種型別。如果你想要了解在 Scala 中更多、更深入的用例,或許可以看看 Kris Nuttycombe 的 F-Bounded Type Polymorphism Considered Tricky 。
想象你有某個 Fruit
特質,一個 Apple
和 Orange
繼承了它。Fruit 特質同時還有一個 compareTo 方法,這時候問題出現了 — 猜想你想說「我不能拿橘子和蘋果進行比較啊,它們可是完全不同的東西!」。我們先來寫一段天真的實現程式碼:
// naive impl, Fruit is NOT self-recursively parameterisedtrait Fruit {
final def compareTo(other: Fruit): Boolean = true // impl doesn't matter in our example, we care about compile-time
}class Apple extends Fruitclass Orange extends Fruitval apple = new Apple()val orange = new Orange()apple compareTo orange // compiles, but we want to make this NOT compile!複製程式碼
在這段程式碼中,由於 Fruit
特質不知道誰會繼承它,所以不可能通過限制 compareTo 的簽名來實現只允許傳入跟this
相同的子型別引數。讓我們利用「自遞迴型別引數」來重新實現下:
trait Fruit[T <
: Fruit[T]] {
final def compareTo(other: Fruit[T]): Boolean = true // impl doesn't matter in our example
}class Apple extends Fruit[Apple]class Orange extends Fruit[Orange]val apple = new Appleval orange = new Orange複製程式碼
注意 Fruit 簽名裡的型別引數,你可以解讀為「我傳入了型別 T
, T
必須是一個 Fruit[T]
」,必須像上述 Apple
和 Orange
一樣繼承這個特質才能滿足這種界限條件。現在如果我們要比較 apple
和 orange
,我們就會得到一個編譯時錯誤:
scala>
orange compareTo apple:13: error: type mismatch;
found : Apple required: Fruit[Orange] orange compareTo applescala>
orange compareTo orangeres1: Boolean = true複製程式碼
因此我們確定只能在同類水果之間進行比較,比如蘋果跟蘋果。假使討論更多,要是 Apple
和 Orange
的子類呢?好,因為我們在型別繼承關係中在 Apple / Orange 層填寫了型別引數,根本上我們可以說「蘋果只能跟蘋果進行比較」,這也意味著蘋果的子類可以進行相互比較。這對 Fruit 的 compareTo
的簽名來說依舊好辦,因為我們呼叫的右側部分會變成 Fruit[Apple]
— 變得更具體一點而已。讓我們用一個日本的蘋果(ja. "
りんご"
, "
ringo"
)和一個波蘭的蘋果(pl. "
Jabłuszko"
)舉例:
object `りんご` extends Appleobject Jabłuszko extends Apple`りんご` compareTo Jabłuszko// true複製程式碼
你也可以通過其它奇技淫巧來實現同樣的型別安全,比如路徑依賴型別、隱式引數或 Type Class ,但這應該是最簡單的實現方式。
13. 型別構造器
❌ 該章節作者尚未完成,或需要修改
型別構造器跟函式幾乎是類似的,但前者是在型別層面。換句話說,你在日常的程式設計中可以給一個函式傳入一個值 a
,然後返回一個值 b
。於是在型別層面程式設計,你可以認為一個 List[+A]
是一個型別構造器,表現如下:
List[+A]
有一個型別引數 (A
);- 它本身並不是一個有效的型別,你需要填充
A
所在的地方來「構造型別」; - 填上
Int
你就得到了一個具體的型別List[Int]
。
通過這個例子,你會發現「型別構造器」跟「普通構造器」是如此的相似,唯一不同的地方在於前者處理的是型別,而不是物件的例項。值得注意的是,在 Scala 中我們不能說某個東西的型別是 List
,因為它並不像 Java 裡,javac 會將 List<
給你。 Scala 在這個地方是更嚴格的,它並不允許我們僅僅使用一個
Object>List
來代表一個型別,因為它是一個型別構造器,而不是一個真正的具體型別。
在 Scala 2.11.x 中我們將在 REPL 中擁有一個強大的命令 – :kind
,它支援檢測一個型別是高階。讓我們通過一個簡單的型別構造器來試試看,比如 List[+A]
:
// Welcome to Scala version 2.11.0-M5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0-ea).// Type in expressions to have them evaluated.:kind List// scala.collection.immutable.List's kind is F[+A]:kind -v List// scala.collection.immutable.List's kind is F[+A]// * -(+)->
*// This is a type constructor: a 1st-order-kinded type.複製程式碼
這裡我們看到,scalac 可以告訴我們 List
實際上是一個型別構造器(當與 -verbose
一起使用時,會更有說服力)。我們來調查下上述資訊中的語法:* ->
。這個語法被廣泛地用於代表型別( kind ,而不是 type ),我發現事實上這是受到了 Haskell 的啟發 — Haskell 用它來列印函式的型別。最直觀的解讀是「傳入一個型別,返回另一個型別」。你也許已經注意到我們從 Scala 完整的輸出中省略了來自關係中的
*+
符號(* -(+)->
)。這個代表型變的邊界,你可以在 Scala 中的型變 一節中瞭解更多關於型變的內容。
*
綜上所述,List[A+]
(或者 Option[+A]
,或者 Set[+A]
…… 或者其它有一個型別引數的東西)是最簡單的型別構造器的例子 — 這些都有一個引數。我們稱它們為第一階型別 (* ->
)。值得一提的是,一個
*Pair[+A, +B]
(我們可以表示為 * ->
)依舊不是一個「高階型別」,它也是第一階的。在下一節中,我們將仔細研究高階型別到底給我們帶來了什麼,以及如何識別它。
* ->
*
14. 高階型別
❌ 該章節作者尚未完成,仍舊缺失部分內容
這裡一個典型的例子是 Monad
:
scala>
import scalaz._import scalaz._scala>
:k Monad // Finds locally imported types.Monad's kind is (* ->
*) ->
*This is a type constructor that takes type constructor(s): a higher-kinded type.複製程式碼
TODO:blogs.atlassian.com/2013/09/sca…
15. 樣例類
樣例類(Case Class)是 Scala 編譯器中最有用的小技巧之一。它使用起來簡單,然而又幫了大忙。它為我們避免了一些非常重複無聊的工作,如 equals
、hashCode
和 toString
的實現,內建了 apply
/ unapply
方法來支援模式匹配,等等。
在 Scala 中一個樣例類的宣告就像一個普通的類一樣,只是需要前置一個 case
關鍵字:
case class Circle(radius: Double)複製程式碼
僅一行程式碼,我們就已經實現了 Value Object 模式。這意味著通過定義一個樣例類,我們就自動做到了以下事情:
- 它的例項是不可變的;
- 可以使用
equals
來被比較,通過它的欄位來判定相等(而不是類似一個普通類的物件相等); - 它的
hashCode
奉行equals
的契約,是基於類的值; - 它的
toString
由類名和它所包含的欄位的值組成的(對照上面的 Circle,可實現為def toString = s"
)。
Circle($radius)"
我們消化下目前所提到的東西,接下來將使用一個生動的例子來繼續延伸。這次我們要實現一個 Point
類,它會擁有不止一個欄位,來展現 case class
給我們提供的一些有趣的特性:
① case class Point(x: Int, y: Int) ② val a = Point(0, 0) ③ // a.toString == "Point(0,0)" ④ val b = a.copy(y = 10) // b.toString == "Point(0,10)"⑤ a == Point(0, 0)複製程式碼
① x
和 y
自動被定義為 val
成員;
② 一個 Point
的伴身物件會同時產生,它有一個 apply(x: Int, y: Int)
方法,我們可以藉此建立例項;
③ 生成的 toString
方法包含了類名以及 case class 的引數值;
④ copy(...)
方法支援我們輕鬆建立拷貝的物件,改變選定的欄位;
⑤ case class 基於值來判等 ( equals
和 hashCode
被生成,它們基於 case class 的引數實現)
除此之外,一個 case class 還可被用於模式匹配,使用慣常的或者「抽取器模式」語法:
Circle(2.5) match {
case Circle(r) =>
println("Radius = " + r)
}val Circle(r)val r2 = r + r複製程式碼