上一章介紹了類型類的概念,這種模式使設(shè)計(jì)出來的程序既擁抱擴(kuò)展性,又不放棄具體的類型信息。這一章,我們還將繼續(xù)探究 Scala 的類型系統(tǒng),講講另一個(gè)特性,這個(gè)特性可以將 Scala 與其他主流編程語言區(qū)分開:依賴類型,特別是,路徑依賴的類型和依賴方法類型。
一個(gè)廣泛用于反對(duì)靜態(tài)類型的論點(diǎn)是 “the compiler is just in the way”,最終得到的都是數(shù)據(jù),為什么還要建立一個(gè)復(fù)雜的類型層次結(jié)構(gòu)?
到最后,靜態(tài)類型的唯一目的就是,讓“超級(jí)智能”的編譯器來定期“羞辱”編程人員,以此來預(yù)防程序的 bug,在事情變得糟糕之前,保證你做出正確的選擇。
路徑依賴類型是一種強(qiáng)大的工具,它把只有在運(yùn)行期才知道的邏輯放在了類型里,編譯器可以利用這一點(diǎn)減少甚至防止 bug 的引入。
有時(shí)候,意外的引入路徑依賴類型可能會(huì)導(dǎo)致難堪的局面,尤其是當(dāng)你從來沒有聽說過它。因此,了解和熟悉它絕對(duì)是個(gè)好主意,不管以后要不要用。
先從一個(gè)問題開始,這個(gè)問題可以由路徑依賴類型幫我們解決:在同人小說中,經(jīng)常會(huì)發(fā)生一些駭人聽聞的事情。比如說,兩個(gè)主角去約會(huì),即使這樣的情景有多么的不合常理,甚至還有穿越的同人小說,兩個(gè)來自不同系列的角色互相約會(huì)。
不過,好的同人小說寫手對(duì)此是不屑一顧的。肯定有什么模式來阻止這樣的錯(cuò)誤做法。下面是這種領(lǐng)域模型的初版:
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
類有一個(gè)方法,這個(gè)方法用來創(chuàng)建有關(guān)兩個(gè)角色的小說。下面代碼創(chuàng)建了兩個(gè)系列和一些角色:
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")
不幸的是,這一刻,我們無法阻止不好的事情發(fā)生:
starTrek.createFanFiction(lovestruck = jadzia, objectOfDesire = luke)
多么恐怖的事情!某個(gè)人創(chuàng)建了一段同人小說,婕琪戴克斯和天行者盧克竟然在約會(huì)!我們不應(yīng)該容忍這樣的事情。
婕琪戴克斯:星際迷航中的角色:http://en.wikipedia.org/wiki/Jadzia_Dax天行者盧克:星球大戰(zhàn)中的角色:http://en.wikipedia.org/wiki/Luke_Skywalker
你的第一直覺可能是,在運(yùn)行期做一些檢查,保證約會(huì)的兩個(gè)角色來自同一個(gè)特許商。比如說:
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)
}
}
現(xiàn)在,每個(gè)角色都有一個(gè)指向所屬發(fā)行商的引用,試圖創(chuàng)建包含不同系列角色的小說會(huì)引發(fā) IllegalArgumentException
異常。
這挺好,不是嗎?畢竟這是被灌輸多年的行為方式:快速失敗。然而,有了 Scala,我們能做的更好。有一種可以更快速失敗的方法,不是在運(yùn)行期,而是在編譯期。為了實(shí)現(xiàn)它,我們需要將 Character
和它的 Franchise
之間的聯(lián)系編碼在類型層面上。
Scala 嵌套類型 工作的方式允許我們這樣做。一個(gè)嵌套類型被綁定在一個(gè)外層類型的實(shí)例上,而不是外層類型本身。這意味著,如果將內(nèi)部類型的一個(gè)實(shí)例用在包含它的外部類型實(shí)例外面,會(huì)出現(xiàn)編譯錯(cuò)誤:
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
不能簡(jiǎn)單的將綁定在 a2
上的類型 B
的實(shí)例賦值給 a1
上的字段:前者的類型是 a2.B
,后者的類型是 a1.B
。中間的點(diǎn)語法代表類型的路徑,這個(gè)路徑通往其他類型的具體實(shí)例。因此命名為路徑依賴類型。
下面的代碼運(yùn)用了這一技術(shù):
class Franchise(name: String) {
case class Character(name: String)
def createFanFictionWith(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)
}
這樣,類型 Character
嵌套在 Franchise
里,它依賴于一個(gè)特定的 Franchise
實(shí)例。
重新創(chuàng)建幾個(gè)角色和發(fā)行商:
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")
把角色放在一起構(gòu)成小說:
starTrek.createFanFictionWith(lovestruck = quark, objectOfDesire = jadzia)
starWars.createFanFictionWith(lovestruck = luke, objectOfDesire = yoda)
順利編譯!接下來,試著去把 jadzia
和 luke
放在一起:
starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)
不應(yīng)該的事情就會(huì)編譯失敗!編譯器抱怨類型不匹配:
found : starWars.Character
required: starTrek.Character
starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)
即使這個(gè)方法不是在 Franchise
中定義的,這項(xiàng)技術(shù)同樣可用。這種情況下,可以使用依賴方法類型,一個(gè)參數(shù)的類型信息依賴于前面的參數(shù)。
def createFanFiction(f: Franchise)(lovestruck: f.Character, objectOfDesire: f.Character) =
(lovestruck, objectOfDesire)
可以看到, lovestruck
和 objectOfDesire
參數(shù)的類型依賴于傳遞給該方法的 Franchise
實(shí)例。不過請(qǐng)注意:被依賴的實(shí)例只能在一個(gè)單獨(dú)的參數(shù)列表里。
依賴方法類型通常和抽象類型成員一起使用。假設(shè)我們?cè)陂_發(fā)一個(gè)鍵值存儲(chǔ),只支持讀取和存放操作,但是類型安全的。下面是一個(gè)簡(jiǎn)化的實(shí)現(xiàn):
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)
}
我們定義了一個(gè)含有抽象類型成員 Value
的類 Key
。AwesomeDB
中的方法可以引用這個(gè)抽象類型,即使不知道也不關(guān)心它到底是個(gè)什么表現(xiàn)形式。
定義一些想使用的具體的鍵:
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
}
之后,就可以存放鍵值對(duì)了:
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 代碼中,路徑依賴類型并不是那么無處不在,但它確實(shí)是有很大的實(shí)踐價(jià)值的,除了給同人小說建模之外。
最普遍的用法是和 cake pattern 一起使用,cake pattern 是一種組件組合和依賴管理的技術(shù)。冠以這一點(diǎn),可以參考 Debasish Ghosh 的 文章 。
把一些只有在運(yùn)行期才知道的信息編碼到類型里,比如說:異構(gòu)列表、自然數(shù)的類型級(jí)別表示,以及在類型中攜帶大小的集合,路徑依賴類型和依賴方法類型有著至關(guān)重要的角色。Miles Sabin 正在 Shapeless 中探索 Scala 類型系統(tǒng)的極限。
更多建議: