[Scala] Function 與 Function 間的子類別關係。

前言

說真的,雖然高中的時候接觸過一點 C / Pascal,大學的時候正式接觸 C / Java 來寫程式,但還是一直到開始玩 Scala 之後,才知道原來一個函式可以是另一個函式的子類別。

只是就算有了這個粗淺的概念,也大致上知道什麼是 covariant / contravariant / invariant,但其實對於這個函式與函式之前的子類別關係一直都是一知半解的,一直到這個月上了 Coursera 網站上的 Functional Programming Principles in Scala 後,才終於搞懂函式與函式之間的子類別是什麼意思。

所以趁著腦袋還沒變鈍之前,來寫一篇筆記,記錄我理解這個問題的方式,方便以後查看復習。 XDD

普通物件的繼承關係與 Polymorphism

如果用過 C++ / Java 等物件導向的程式語言,那麼對於普通物件的繼承關係應該會很熟悉,大部書上最常舉的例子會像下面這樣:

class Animal {
  def eat() = println("I'm eating...")
}

class Dog extends Animal {
  override def eat() = println("I'm eating my dog food")
}

class Fish extends Animal {
  override def eat() = println("I'm eating my fish food")
  def swim() = println("I'm swimming...")
}

由於 Dog 和 Fish 都繼承自 Animal,所以 Dog 和 Fish 是 Animal 的子類別,這也就表示所有你可以對 Animal 做的事,都可以對 Dog 和 Fish 做,但 Fish 和 Dog 能做的,Animal 不一定能做。

換句話說,你可以把 Dog 和 Fish 的物件 assign 給 Animal 型態的 reference 變數,但不能把 Animal 型態的物件 assign 給 Dog 和 Fish 型態的 reference 變數:

val animal1: Animal = new Dog  // OK
val animal2: Animal = new Fish // OK
animal1.eat()                  // OK,Dog 一定有 eat() 可以用
animal2.eat()                  // OK,Fish 一定有 eat() 可以用

val fish: Fish = new Animal    // Compile Error,Animal 不一定是 Fish
val dog: Dog = new Animal      // Compile Error,Animal 不一定是 Dog
fish.swim()                    // Animal 不一定有 swim() 可以用

從上面的程式碼可以看出來,可以把 Dog 和 Fish 當成 Animal 的原因,主要是不論是 Dog 或 Fish 都有 eat() 這個方法可以用。也就是說先決條件,是可以對 Animal 做的事情,都要可以對 Dog 和 Fish 來做,這樣才可以把 Dog 和 Fish 視為 Animal 的子類別。

如果用比較正式的 Liskov Substitution Principle 定義來講,就是:

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

可以對函式做什麼?

從上面我們已經看到,要說 B 是 A 的子類別,其中一個非常重要的條件,是所有可以對 A 做的事情,都要可以對 B 做,我們才能說 B 是 A 的子類別。

那我們可以對一個 Function 做什麼事呢?嗯……丟參數進去,並且取得返回的值。

所以假設我們有以下的函式:

val findAnimal: Fish => Animal = (fish) => new Animal

在 Scala 中像這樣宣告的話,findAnimal 會是一個函式,他接受 Fish 型別的物件,並且返回一個 Animal 或其子類別的物件,也就是說,針對 findAnimal 我們可以做以下的事情:

  1. 丟一個 Fish 型態的東西進去
  2. 拿回一個 Animal 型態的東西

現在好玩的地方來了,如果我們今天有另一個函式長得像這個樣子:

val findFish: Animal => Fish = (animal) => new Fish

那我們可以對這個函式做什麼事情呢?嗯……一開始很明顯的,我們一定可以:

  1. 丟一個 Animal 型態的東西進去
  2. 拿回一個 Fish 型態的東西

但如果再靠近一點看,這個函式能不能做到下面的事呢?

  1. 丟一個 Fish 型態的東西進去
  2. 拿回一個 Animal 型態的東西

可以耶!因為 Fish 比 Animal 更明確,所以一定可以丟到 findFish 裡面去,而 findFish 的回傳值是一個 Fish,Fish 又是 Animal 的子類別,所以我們拿回來的也可以算是 Animal 型態的東西。

Scala 裡函式的子類別關係與 Polymorphism

等等,傳一個 Fish 型態並拿回一個 Animal 型態,這不就是 findAnimal 在做的事情嗎?

那根據 Liskov Substitution Principle 來說,Animal => Fish 這種函式就會是 Fish => Animal 的子類別,而如果 B 是 A 的子類別的話,我們可以把型態是 B 的物件 assign 給型態是 A 的 reference 變數……嗯,我們知道 Scala 裡面 Function 是可以 assign 給變數的,但他可以把一個 Function 當成另一個 Function 的子類別嗎?

來試試看:

class Animal
class Dog extedns Animal
class Fish extends Animal

val findAnimal: Fish => Animal = (fish) => {println ("From findAnimal"); new Animal }
val findFish: Animal => Fish = (animal) => {println ("From findFish"); new Fish }
val findAnimalByDog: Dog => Animal = (dog) => {println("From findDog"); new Dog }

val testAnimal: Fish => Animal =   // 假設我們今天要一個吃 Fish 吐 Animal 的函式
    findFish                       // 但我們給一個吃 Animal 吐 Fish 的函式

testAnimal(new Fish)               // 結果可以用耶!印出來的是 From findFish

// 但你不能這樣用,會編譯錯,吃狗的函式不吃魚
val testAnimal: Fish => Animal = findAnimalByDog

喔喔喔喔!函式竟然可以是另一個函式的子類別,而且還可以用 Polymorphism 的方式把 B 型態的函式當成 A 型態的函式來用耶,我以前從來沒想過可以這樣搞。XD

用正式的定義來講就是:

當有函式 C => D 與函式 A => B 存在時,若 C 是 A 的父類別且 D 是 B 的子類別,則 C => D 是 A => B 的子類別。

很難記,我自己也記不住,所以後來我發現比較簡單的記法是:

如果有 E 和 F 兩個函式,如果 E 吃的東西比 F 來的廣,但吐出來的東西比 F 來的窄,那 E 就是 F 的子類別,因為任何 F 可以吃的東西 E 都可以吃,而任何 E 回傳來的東西,都一定符合 F 的要求。

Scala 怎麼做到的?

但 Scala 是怎麼做到的呢?很簡單,在 Scala 裡面上面的函式通通是物件,他長得大概像下面這樣子(實際上複雜很多,這裡並不是列出 Scala 真的在使用的 Function 程式碼):

trait Function[T1, R] {
  def apply(param: T1): R
}

換句話說,當你寫下了像下面的程式碼的時候:

val x: Int => String = (x: Int) = (x + 1).toString
val y = x(10)

Scala 的編譯器是編譯成下面的東西:

val x: Function[Int, String] { def apply(x: Int) = (x + 1).toString }
val y = x.apply(10)

但如果我們自己試著模擬這個做法,會發現怪怪的……

class Animal
class Dog extends Animal
class Fish extends Animal

class MyFunction[T1, R]

val findAnimal: MyFunction[Fish, Animal] = new MyFunction
val findAnimalByDog: MyFunction[Dog, Animal] = new MyFunction
val findFish: MyFunction[Animal, Fish] = new MyFunction

val testAnimal: MyFunction[Fish, Animal] = findFish

什麼?竟然出現了編譯錯誤,告訴我 Type-mismatch ???

scala>    val testAnimal: MyFunction[Fish, Animal] = findFish
<console>:11: error: type mismatch;
 found   : MyFunction[Animal,Fish]
 required: MyFunction[Fish,Animal]

難不成 Scala 自己在 Function 裡面做了什麼手腳?就某方面來說沒錯……如果你去翻 Scala 的原始碼,就會發現實際上 Function 物件的類別是長這樣的(trait 可以視為 Java 裡的 interface):

trait Function1[-T1, +R] {
  // ....
}

其怪?為什麼 Type-Parameter 前面會多了減號和加號?這又代表了什麼意思呢?

其實這是 Scala 強大的(你也可以說複雜 XD)的型別系統提供的進階功能,叫做 Varance Control,可以控制在泛型的情況下,怎樣的型別可以算是另一個型別的子類別。

總共會有三種情況:

  1. 型別前什麼都沒有符號都沒有,代表他是 invariant,也就是說 MyType[T1] 和 MyType[T2],不論 T1 和 T2 的關係是什麼,MyType[T1] 和 MyType[T2] 都沒有子類別的關聯。
class Animal
class Dog extends Animal
class MyType[T]

// 編譯錯誤,Type-mismatch
//
// 雖然 Dog 是 Animal 的子類別,但 MyType[Animal] 和 MyType[Dog] 是沒關聯的
//
val temp: MyType[Animal] = new MyType[Dog]
  1. 型別前有 + 號,代表若 T2 是 T1 的子類別,則 MyType[T2] 是 MyType[T1] 的子類別,這個叫 covariant。
class Animal
class Dog extends Animal
class MyType[+T]

// OK,因為 Dog 是 Animal 的子類別,所以 MyType[Dog] 是 MyType[Animal] 的子類別
val temp1: MyType[Animal] = new MyType[Dog]

// 編譯錯誤,因為 MyType[Animal] 是父類別,不能 assign 給子類別的 reference 變數,
// 就像我們不能把 Dog 物件指定給 Animal 型別的 reference 變數
val temp2: MyType[Dog] = new MyType[Animal]
  1. 型別前有 - 號,代表若 T2 是 T1 的父類別,則 MyType[T2] 是 MyType[T1] 的子類別,這個叫 contravariant。
class Animal
class Dog extends Animal
class MyType[-T]

// OK,因為 Animal 是 Dog 的父類別,所以 MyType[Dog] 是 MyType[Animal] 的子類別
val temp1: MyType[Dog] = new MyType[Animal]

// 編譯錯誤,由於前面是 - 號,所以關係反過來了,MyType[Dog] 才是父類別
val temp2: MyType[Animal] = new MyType[Dog]

有了這個 Varaince Control 的工具,我們就可以在程式碼裡表示出這個規則了:

當有函式 C => D 與函式 A => B 存在時,若 C 是 A 的父類別且 D 是 B 的子類別,則 C => D 是 A => B 的子類別。

注意到了嗎?C 是 A 的父類別,而 D 是 B 的子類別,也就是說我們可以用 [-T, +R] 來表式這樣的關係,所以如果我們把程式碼改成下面這樣:

class Animal
class Dog extends Animal
class Fish extends Animal

class MyFunction[-T, +R]

那下面的程式碼就可以動了,我們成功模擬出函式與函式之間的繼承關係了耶!

val findAnimal: MyFunction[Fish, Animal] = new MyFunction
val findAnimalByDog: MyFunction[Dog, Animal] = new MyFunction
val findFish: MyFunction[Animal, Fish] = new MyFunction

val testAnimal: MyFunction[Fish, Animal] = findFish

結論

Scala 的型別系統真的非常強大,可以表示出很多其他常用的程式語言表示不出來的型別限制,並且讓你得到編譯時期的型別檢查。

但也因為真的太過強大,並且多數是使用符號來表示這些型別的功能,所以很多時候第一眼看到的時候,真的會覺得看起來像外星文,不懂他到底在搞什麼啊……orz.

但一但深入了解這些符號到底代表什麼後,就會發現他的型別系統裡面很多設計都很漂亮,讓你可以做很多神奇的事情。:p

回響