路徑依賴類型

2018-02-24 16:00 更新

上一章介紹了類型類的概念,這種模式使設(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)

順利編譯!接下來,試著去把 jadzialuke 放在一起:

    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)

可以看到, lovestruckobjectOfDesire 參數(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

實(shí)踐中的路徑依賴類型

在典型的 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)的極限。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)