上一章介紹了類型類的概念,這種模式使設計出來的程序既擁抱擴展性,又不放棄具體的類型信息。這一章,我們還將繼續探究 Scala 的類型系統,講講另一個特性,這個特性可以將 Scala 與其他主流編程語言區分開:依賴類型,特別是,路徑依賴的類型和依賴方法類型。
一個廣泛用于反對靜態類型的論點是 “the compiler is just in the way”,最終得到的都是數據,為什么還要建立一個復雜的類型層次結構?
到最后,靜態類型的唯一目的就是,讓“超級智能”的編譯器來定期“羞辱”編程人員,以此來預防程序的 bug,在事情變得糟糕之前,保證你做出正確的選擇。
路徑依賴類型是一種強大的工具,它把只有在運行期才知道的邏輯放在了類型里,編譯器可以利用這一點減少甚至防止 bug 的引入。
有時候,意外的引入路徑依賴類型可能會導致難堪的局面,尤其是當你從來沒有聽說過它。因此,了解和熟悉它絕對是個好主意,不管以后要不要用。
### 問題
先從一個問題開始,這個問題可以由路徑依賴類型幫我們解決:在同人小說中,經常會發生一些駭人聽聞的事情。比如說,兩個主角去約會,即使這樣的情景有多么的不合常理,甚至還有穿越的同人小說,兩個來自不同系列的角色互相約會。
不過,好的同人小說寫手對此是不屑一顧的。肯定有什么模式來阻止這樣的錯誤做法。下面是這種領域模型的初版:
~~~
object Franchise {
case class Character(name: String)
}
class Franchise(name: String) {
import Franchise.Character
def createFanFiction(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)
}
~~~
角色用 `Character` 樣例類表示, `Franchise` 類有一個方法,這個方法用來創建有關兩個角色的小說。下面代碼創建了兩個系列和一些角色:
~~~
val starTrek = new Franchise("Star Trek")
val starWars = new Franchise("Star Wars")
val quark = Franchise.Character("Quark")
val jadzia = Franchise.Character("Jadzia Dax")
val luke = Franchise.Character("Luke Skywalker")
val yoda = Franchise.Character("Yoda")
~~~
不幸的是,這一刻,我們無法阻止不好的事情發生:
~~~
starTrek.createFanFiction(lovestruck = jadzia, objectOfDesire = luke)
~~~
多么恐怖的事情!某個人創建了一段同人小說,婕琪戴克斯和天行者盧克竟然在約會!我們不應該容忍這樣的事情。
> 婕琪戴克斯:星際迷航中的角色:[http://en.wikipedia.org/wiki/Jadzia_Dax](http://en.wikipedia.org/wiki/Jadzia_Dax)天行者盧克:星球大戰中的角色:[http://en.wikipedia.org/wiki/Luke_Skywalker](http://en.wikipedia.org/wiki/Luke_Skywalker)
你的第一直覺可能是,在運行期做一些檢查,保證約會的兩個角色來自同一個特許商。比如說:
~~~
object Franchise {
case class Character(name: String, franchise: Franchise)
}
class Franchise(name: String) {
import Franchise.Character
def createFanFiction(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = {
require(lovestruck.franchise == objectOfDesire.franchise)
(lovestruck, objectOfDesire)
}
}
~~~
現在,每個角色都有一個指向所屬發行商的引用,試圖創建包含不同系列角色的小說會引發 `IllegalArgumentException` 異常。
### 路徑依賴類型
這挺好,不是嗎?畢竟這是被灌輸多年的行為方式:快速失敗。然而,有了 Scala,我們能做的更好。有一種可以更快速失敗的方法,不是在運行期,而是在編譯期。為了實現它,我們需要將 `Character` 和它的 `Franchise` 之間的聯系編碼在類型層面上。
Scala **嵌套類型** 工作的方式允許我們這樣做。一個嵌套類型被綁定在一個外層類型的實例上,而不是外層類型本身。這意味著,如果將內部類型的一個實例用在包含它的外部類型實例外面,會出現編譯錯誤:
~~~
class A {
class B
var b: Option[B] = None
}
val a1 = new A
val a2 = new A
val b1 = new a1.B
val b2 = new a2.B
a1.b = Some(b1)
a2.b = Some(b1) // does not compile
~~~
不能簡單的將綁定在 `a2` 上的類型 `B` 的實例賦值給 `a1` 上的字段:前者的類型是 `a2.B` ,后者的類型是 `a1.B` 。中間的點語法代表類型的路徑,這個路徑通往其他類型的具體實例。因此命名為路徑依賴類型。
下面的代碼運用了這一技術:
~~~
class Franchise(name: String) {
case class Character(name: String)
def createFanFictionWith(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)
}
~~~
這樣,類型 `Character` 嵌套在 `Franchise` 里,它依賴于一個特定的 `Franchise` 實例。
重新創建幾個角色和發行商:
~~~
val starTrek = new Franchise("Star Trek")
val starWars = new Franchise("Star Wars")
val quark = starTrek.Character("Quark")
val jadzia = starTrek.Character("Jadzia Dax")
val luke = starWars.Character("Luke Skywalker")
val yoda = starWars.Character("Yoda")
~~~
把角色放在一起構成小說:
~~~
starTrek.createFanFictionWith(lovestruck = quark, objectOfDesire = jadzia)
starWars.createFanFictionWith(lovestruck = luke, objectOfDesire = yoda)
~~~
順利編譯!接下來,試著去把 `jadzia` 和 `luke` 放在一起:
~~~
starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)
~~~
不應該的事情就會編譯失敗!編譯器抱怨類型不匹配:
~~~
found : starWars.Character
required: starTrek.Character
starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)
~~~
即使這個方法不是在 `Franchise` 中定義的,這項技術同樣可用。這種情況下,可以使用依賴方法類型,一個參數的類型信息依賴于前面的參數。
~~~
def createFanFiction(f: Franchise)(lovestruck: f.Character, objectOfDesire: f.Character) =
(lovestruck, objectOfDesire)
~~~
可以看到, `lovestruck` 和 `objectOfDesire` 參數的類型依賴于傳遞給該方法的 `Franchise` 實例。不過請注意:被依賴的實例只能在一個單獨的參數列表里。
### 抽象類型成員
依賴方法類型通常和抽象類型成員一起使用。假設我們在開發一個鍵值存儲,只支持讀取和存放操作,但是類型安全的。下面是一個簡化的實現:
~~~
object AwesomeDB {
abstract class Key(name: String) {
type Value
}
}
import AwesomeDB.Key
class AwesomeDB {
import collection.mutable.Map
val data = Map.empty[Key, Any]
def get(key: Key): Option[key.Value] = data.get(key).asInstanceOf[Option[key.Value]]
def set(key: Key)(value: key.Value): Unit = data.update(key, value)
}
~~~
我們定義了一個含有抽象類型成員 `Value` 的類 `Key`。`AwesomeDB` 中的方法可以引用這個抽象類型,即使不知道也不關心它到底是個什么表現形式。
定義一些想使用的具體的鍵:
~~~
trait IntValued extends Key {
type Value = Int
}
trait StringValued extends Key {
type Value = String
}
object Keys {
val foo = new Key("foo") with IntValued
val bar = new Key("bar") with StringValued
}
~~~
之后,就可以存放鍵值對了:
~~~
val dataStore = new AwesomeDB
dataStore.set(Keys.foo)(23)
val i: Option[Int] = dataStore.get(Keys.foo)
dataStore.set(Keys.foo)("23") // does not compile
~~~
### 實踐中的路徑依賴類型
在典型的 Scala 代碼中,路徑依賴類型并不是那么無處不在,但它確實是有很大的實踐價值的,除了給同人小說建模之外。
最普遍的用法是和 **cake pattern** 一起使用,cake pattern 是一種組件組合和依賴管理的技術。冠以這一點,可以參考 Debasish Ghosh 的 [文章](http://debasishg.blogspot.ie/2013/02/modular-abstractions-in-scala-with.html) 。
把一些只有在運行期才知道的信息編碼到類型里,比如說:異構列表、自然數的類型級別表示,以及在類型中攜帶大小的集合,路徑依賴類型和依賴方法類型有著至關重要的角色。Miles Sabin 正在 [Shapeless](https://github.com/milessabin/shapeless) 中探索 Scala 類型系統的極限。