# 你可能其實不懂繼承 - 什麼是 Covariance 跟 Contravariance 在近代高等程式語言快(ㄏㄨˋ)速(ㄒ一ㄤ)演(ㄔㄠ)化(ㄒㄧˊ)的狀況下,很多 strong type 的程式語言都開始有了 generic 的設計, 舉例來說: (這只是大略分類) - Mobile: Swift, Kotlin - Frontend: Flowtype, TypeScript - 後端語言:Python (pyre), Scala, [C#](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-generic-interfaces), Java (沒錯!現在連 Java 都有) Generic type 雖然看起來是比較新的東西,但如果要做到嚴格的 strong type 基本上是少不了的。 例如 Array/List 如果沒有 generic 的話,只能有兩種做法: 1. 把所有東西都轉成 Object or Any type 才能塞進 List,然後在從 List 取出物件的時候再做 down casting。 2. 針對需要的 object type 個別實作相對應的 List type,例如:Cat 就有 CatList。 第一個方法的問題是:你有可能 down cast 到錯的 type,譬如說你本來塞的是 Cat,但你拿出來後 cast 成 Dog。 這樣就會噴 Runtime Error。而第二種方法的確是 type safe ,但因為每個 type 都要重寫一堆邏輯,非常不方便。 現在我們知道 generic 的方便性及重要性了。接下來我們來進入本篇想討論的:Covariance 跟 Contravariance。 基本上, generic 的出現代表了 type system 增加了一個新的維度,碰上有 subtype 的 type system 會變得比平常複雜。 偏偏現代幾乎所有程式語言都有 inheritance,所以這變成了現在 programmer 早晚會碰到的問題。我聽到的 case 都幾乎是走 「碰到就想辦法繞過」這種解決方式。而個人在工作上也有碰到因為對 generic 一知半解而設計出來得詭異架構。要了解什麼是 covariance ,我們必須先了解 generic 碰上 subtype 會出現什麼樣的問題。不過首先,讓我們先複習一下什麼是 subtype。 # 什麼是 Subtype、Supertype 從 Java 派 OOP 的角度來說,如果 A 繼承 B ,我們稱 A 為 B 的 subtype,而 B 為 A 為 supertype。 但因為我們接下來要理解在 generic 的 context 下怎麼去理解 subtype,所以從語法來看並不是一個很泛用的方法。 比較好的方式應該是使用這個定義: > 如果 A 是 B 的 subtype(或 B 是 A 的 supertype),那 B 有的東西、能做的事情,都可以從 A 身上找得到。 例如 Cat 是 Animal 的 subtype,這個很明顯應該不用特別解釋。而 Animal 不是 Cat 的 subtype,其中一個原因是不是所有 Animal 都會喵貓叫。 以下我們用這個 subtype 關係鏈來舉例: - 波斯貓 < Cat < Animal - Dog < Animal 現在我們對 subtype 有共識了,接下來我們可以回來看怎麼來解 generic type 的 subtype 關係。其中最經典的: ## `List` 是不是 `List` 的 subtype? 乍看之下答案是「當然是啊,`Cat` 是 `Animal`,當然 `List` 就是 `List` 啊」,但實際上並沒有這麼簡單。 我們先假設 `List` 是 `List` 這件事情成立好了,這就代表: ```java List listOfCat = // 總之,某個方法生出來的,不是重點所以略過 List listOfAnimal = listOfCat; ``` 以上,根據假設沒問題,但接下來的 code 就不合理了: ```java listOfAnimal.add(new Dog()); ``` 光看這一行可能沒問題,但同時考慮前段 code 就能看出錯誤:`listOfAnimal` 實際上是 `listOfCat`,而其 type 是 `List`。 也就是,你正在把 dog 塞進一個只能放 Cat 的 List,而如果其他地方還持有 `listOfCat` 的 reference 的話,就有可能拿到 dog, 而 dog 不是 cat,當然就是個 type error。 **也就是如果一個 list 允許你「塞」值進去的話,那它就不會是 subtype。** 既然 `List` 不是 `List` 的 subtype,那 ## `List` 能不能是 `List` 的 supertype? 答案是....看情況。 那麼什麼情況下不行呢? 如果你今天試著從 list 拿值出來就不行。 ``` Cat cat = listOfCat.get(0) // 取得 Animal ``` 因為 `listOfCat` 如果是 `listOfAnimal` 的 supertype 的話,代表 `listOfCat` 有可能實際上是 `listOfAnimal`, 也就是你取到的值有可能是 `Animal` 而不是 `Cat`,而 `Animal` 並不是 `Cat` 的 subtype,於是這邊會產生另外一個 type error。 所以以常識上的通用 `List` 來說的話,`List` **既不是** `List` 的 **subtype 也不是 supertype。** 那假設我們想做一個特殊的 `List` 使得 `List` 是 `List` 的 subtype 呢? ## 什麼是 Covariance? > 假設 A 是 B 的 subtype,那麼 `ConstList` 就是 `ConstList` 的 subtype 首先,在上述的例子中我們已經知道如果我們可以「塞」東西進去這個 List , 那就不可能做到「`List` 是 `List` 的 subtype」。 所以我們要另外定一個 list 叫做 ConstList,意思是這個 list 是 constant,也就是我們不能改動裡面的值。 你可能會問:「既然這個 list 不能塞東西進去,那我要這個 list 幹麻?」,雖然不能塞東西進去, 但是你可以在產生這個 list 的時候就把值設定好,例如 ``` ConstList list = new ConstList(cat1, cat2, cat3); ``` 那麼,我們要怎麼告訴 typechecker 說「假設 A 是 B 的 subtype,那麼 `ConstList` 就是 `ConstList` 的 subtype」呢? 以下舉幾個程式語言當例子: FlowType ```flow class ConstList<+T> { ... } ``` (其實我本來想要用 TypeScript 說明的,結果一查資料才發現 TypeScript 在這塊實作其實是有問題的 [issue 1394](https://github.com/Microsoft/TypeScript/issues/1394)) Kotlin ```kotlin class ConstList { ... } ``` Java Java 無法在宣告 class 時宣告是否為 covariant,只能在宣告變數時使用。 也就是在撰寫的時候要特別小心。某種程度上也是算是 type system 設計上的缺失。 ```java ConstList list = new ConstList(); ``` 以上就是所謂的 Covariance:「假設 A 是 B 的 subtype,那麼 `ConstList` 就是 `ConstList` 的 subtype」 ## 什麼是 Contravariance? > 假設 A 是 B 的 subtype,那麼 `Comparator` 就是 `Comparator` 的 subtype 那麼有沒有反過來「假設 A 是 B 的 subtype,那麼 `Comparator` 就是 `Comparator` 的 subtype」的呢? 有!它叫做 Contravariance。 當 generic type variable 只出現在 function input 的時候。 例如 `Comparator`,內含一個 function 可以比較兩個東西的大小, 如果前者比較大則輸出大於 0 的數值,如果一樣就輸出 0,比較小則輸出小於零的數值。 `Comparator` 可以比較 Animal 的大小,而 `Comparator` 可以比較 Cat 的大小。 然後我們就可以來問一樣的問題:`Comparator` 是不是 `Comparator` 的 subtype? 要來回答個問題,我們要來複習一下什麼是 subtype:「如果 A 是 B 的 subtype,那 B 能做到的事情 A 一定都能做到。」 `Comparator` 可以比較 `Animal`,而因為 `Cat` 是 `Animal` ,所以 `Comparator` 當然也可以比較 `Cat`。 所以 `Comparator` 是 `Comparator` 的 subtype。 光這樣講可能不是很清楚,讓我們先舉個反例,寫成 code 的話就是: ``` // FlowType let catComparator: Comparator = // .... let animalComparator: Comparator = // ... animalComparator = catComparator; // 假設 Comparator 是 Comparator 的 subtype animalComparator.compare(animal, animal); ``` 乍看之下沒問題,但實際上是拿 `Comparator` 來比較 `Animal`,方式可能是比較喵喵叫的可愛程度,可是並不是所有 `Animal` 都會喵喵叫。 與 Covariance 相對應,他們分別的寫法是 FlowType ```flow class Comparator<-T> { ... } ``` Kotlin ```kotlin class Comparator { ... } ``` Java 一樣,Java 無法在宣告 class 時宣告是否為 contravariant,只能在宣告變數時使用。 ```java Comparator list = new Comparator(); ``` ## Exercise 如果熟悉 FlowType 的話,這邊有一道題: 為什麼以下 FlowType 程式不能 typechecked?如何修改呢? ``` // FlowType const a: 1 = 1 const b: number = a const c: { [string]: 1 } = { 'a': 1 } const d: { [string]: number } = c; ``` ### Subtype Function? 其實沒有 generic 也有可能碰到 variance 問題。例如: 請問 function `Cat => void` 是不是 function `Animal => void` 的 subtype? ## 奇技淫巧 上面提到,如果可以「塞」東西進去的 generic type 就不能可能是 covariant,這看起來有點像 immutable, 所以有些人會以為寫上 covariant 等於宣告了 immutable data type。的確在某些狀況下是可以這麼做的, 例如上面的 flowtype 練習題。但這並不全然是對的,因為它其實只描述了 subtyping 的關係,而不是 immutability。 那麼在什麼樣的情況下**不能**當 immutable 呢? ``` // FlowType type Cat = { name: string } const cat: Cat = { name: 'Kitty' } const boxedValue: { +cat: Cat } = { cat } boxedValue.cat.name = 'Cookie' ``` 其實跟 `const` (or `val` in Kotlin, Scala, etc) 一樣,他只限制了你不能改這個 reference, 但並沒有限制你不能改這個 value 裡面的內容。 ## 小結 在查找資料後才發現原來到今天為止,號稱 strong type 並有支援 generic 的語言有部分並沒有完善支援, 例如 Java 沒有提供語法可以在宣告 generic type 的時候描述 variance,TypeScript 也是類似狀況。 而 Swift 則沒有支援 variance 語法,奇怪的是 Objective C 有。 因為 generic 會讓 subtyping check 變複雜,如果不用繼承,就能減少 subtyping 造成的 variance 問題。 但是是不是沒有 subtyping 就完全沒有 variance 的問題呢? 答案是否定的,有興趣的朋友可以了解一下 Functor 跟 Contravariant Functor。 希望這篇文章可以幫助你更好地理解 subtyping (繼承)而能更容易地設計出良好的架構。 最後謝謝 @wu_ct 跟 @\_cybai 的 review