真的學不動了: 除了 class , 也該瞭解 Type classes 了

前言

做爲一個 Java 開發者, class 的概念確定是耳熟能詳了,但是在山的另外一邊還有擁有別樣風情的 type classes,但不翻過 Java 這座山,它就始終隔着一層紗。html

一個經典的問題

在編程中,常常須要判斷兩個值是否相等,而在很長的一段時間內這個問題都沒有一個標準的解決方案,這就是經典的判等問題。java

我這裏統一使用 「值」 來代替對象、基本類型等等概念,以便於簡化溝通編程

在 Java 中,咱們能夠用 == ,也能夠用 equals 來判斷值是否相等markdown

public void test() {
    boolean res = "hello" == "world";
  
    boolean res2 = "hello".equals("hello");

    boolean res3 = 3 == 3;

    boolean res4 = 5 == 9;
}
複製代碼

熟悉 Java 的同窗都知道對於非基礎類型, equals 方法的默認實現其實就是調用 == 操做符,而 == 操做比較的是對象的引用地址app

public class Object {
  // ......
  
  public boolean equals(Object obj) {
  	return (this == obj);
  }
  
  // ......
}
複製代碼

全部類都會有 equals 方法,這是由於在 Java 中默認全部類型都是 Object 的子類。框架

其實這也是 Java 語言處理判等問題的解決方案,即統一從 Object 中繼承判等方法。less

image-20200715144724027

但是對於純函數式的語言,好比 Haskell 來講,它沒有 OOP 中的繼承、類等概念,它又該如何優雅的解決判等的問題呢?ide

若是你以爲 Haskell 比較陌生,咱們就換一種提問的方式:還有其它通用的設計方案能夠解決這類判等問題嗎函數

固然有,而 Type classes 就是這個領域內最靚的那個仔,要了解 Type classes, 還得先從多態開始。oop

Type classes 與多態

Type classes 結合了 ad-hoc polymorphism(特設多態)和 Parametric polymorphism (參數化多態),實現了一種更通用的重載。

問題來了,什麼是特設多態、參數化多態呢?

關於多態的更多內容 ,還能夠參考個人前一篇文章《多態都不知道,談什麼對象》

  • ad-hoc polymorphism (特設多態) 指的是函數應用不一樣類型的參數時,會有不一樣的行爲(或者說實現)

    最典型的就是算術重載

    3 * 3  // 表明兩個整形的乘法
    
    3.14 * 3.14 // 表明兩個浮點數的乘法
    複製代碼
  • Parametric polymorphism (參數化多態) 指的是函數被定義在某一些類型之上,對於這些類型來講函數的實現都是同樣的。

    好比 List[T] 的 size() 函數,不管 T 的類型是 String、仍是 Int, size() 的實現都同樣

    List[String].size()
    List[Int].size()
    複製代碼

雖然 Type classes 結合了兩種多態類型,但它自己卻被歸到特設多態(ad-hoc polymorphism)這一分類下。

若是你想了解更多 type classes 的思想,很是推薦閱讀 《How to make ad-hoc polymorphism less ad hoc》 這篇論文,它也算是 Type classes 的開篇做。

Haskell 與 Type classes

Type classes 通常譯做類型類,最開始是由 haskell 引入並實現,因此咱們頗有必要先了解一下 haskell 中的 Type classes。

以最開始提到的判等問題爲例,來看看在 Haskell 中怎麼用 Type classes 去解決。

首先咱們得用關鍵字 class 定義一個 Type class,千萬不要和 Java 的 class 混爲一談。

class Eq a where
	(==) :: a -> a -> Bool
	(/=) :: a -> a -> Bool
複製代碼

/= 其實就是 !=

haskell 的 Type class 與 Java 的 Interface 相似,上面的 Eq 類型類就定義了 ==/= 兩個抽象函數,其中的 a 就是類型變量,與 Java 中的泛型相似。

由此看來,Type classes 只是抽象了一些共同的行爲,而這些行爲的具體實現會根據類型的不一樣而不一樣,具體的實現會由類型類實例來定義。

經過 instance 關鍵字能夠建立類型類實例,下面展現了針對於於 Float 和 Int 的 Eq 類型類實例

instance Eq Int where
	(==) = eqInt
	(/=) = neInt
	
instance Eq Float where
	(==) = eqFloat
	(/=) = neFloat

複製代碼

咱們假設 eqInt、neInt、eqFloat、neFloat 都已經由標準庫實現了

這樣就能夠直接用 ==/= 函數對 Int 和 Float 進行判等了

-- 判斷 Int 的相等性
== 1 2
/= 2 4

-- 判斷 Float 的相等性
== 1.2 1.2
/= 2.4 2.1
複製代碼

在調用 ==/= 函數時,編譯器會根據參數類型自動找到類型類實例,而後調用類型類實例的函數執行調用。

若是用戶須要自定義判等函數,只須要實現本身的類型類實例便可。

此時你可能會不自覺的和最開始提到的繼承方案作一個對比,我畫了兩個圖,能夠參考一下

  • 繼承方案的類型結構是一個層次型的

  • Type classes 方案的類型結構是線性的

若是僅僅從結構上來看的話,它們之間的差異就像 ComparableComparator 同樣。

Scala 與 Type classes Pattern

目前的 Java 是沒法實現 Type classes 的,但同爲 JVM 的語言,多範式的 Scala 卻能夠實現。

與 Haskell 不同, Type classes 在 Scala 中並非一等公民,也就是沒有直接的語法支持,但藉助於強大的隱式系統咱們也能實現 Type classes,因爲實現的步驟比較公式化,也就被稱之爲 Type classes Pattern (類型類模式)。

在 Scala 中實現 Type classes Pattern 大體分爲 3 個步驟

  1. 定義 Type class
  2. 實現 Type class 實例
  3. 定義包含隱式參數的函數

仍是之前面提到的判等問題爲需求,按照前面總結的模式步驟來實現一個 Scala 版的 Type classes 解決方案。

第一步定義 Type class,實際就是定義一個帶泛型參數的 trait

trait 也相似於 Java 的 interface,不過更增強大

trait Eq[T] {
  def eq(a: T, b: T): Boolean
}
複製代碼

接着咱們針對 String、Int 來實現兩個類型類實例

object EqInstances {

  implicit val intEq = new Eq[Int] {
    override def eq(a: Int, b: Int) = a == b
  }

  implicit val stringEq = instance[String]((a, b) => a.equals(b))

  def instance[T](func: (T, T) => Boolean): Eq[T] = new Eq[T] {
    override def eq(a: T, b: T): Boolean = func(a, b)
  }
}
複製代碼

stringEq 和 intEq 採用了不一樣的構造方式

  • stringEq 實例我採用的是相似於 Java 的匿名類進行構造
  • intEq 實例則採用了高階函數來實現

兩個實例都被 implicit 關鍵字修飾,通常稱之爲隱式值,做用會在後面講到。

最後一步,來實現一個帶隱式參數的 same 函數, 其實調用類型類實例來判斷兩個值是否相等

object Same {
  def same[T](a: T, b: T)(implicit eq: Eq[T]): Boolean = eq.eq(a, b)
}

複製代碼
  • implicit eq: Eq[T] 就是隱式參數, 調用方能夠不用主動傳入,編譯器會在做用域內查找匹配的隱式值傳入(這就是爲何前面的實例須要被 implicit 修飾)

最後來進行調用驗證一下,在調用時咱們須要先在當前做用域內經過 import 關鍵字導入類型類實例(主要是爲了讓編譯器能找到這些實例)

import EqInstances._

Same.same(1, 2)

Same.same("ok", "ok")

// 編譯錯誤:no implicits found for parameter eq: Eq[Float]
Same.same(1.0F, 2.4F)
複製代碼

能夠看見,針對 Int 和 String 類型的 same 函數調用能經過編譯, 而當參數是 Float 時調用就會提示編譯錯誤,這就是由於編譯器在做用域內沒有找到能夠處理 Float 類型的 Eq 實例。

關於 Scala 隱式查找的更多規則能夠查看 docs.scala-lang.org/tutorials/F…

到這兒其實就差很少了,可是這樣的寫法在 Scala 裏其實不是很優雅,咱們能夠再經過一些小技巧優化一下

  • same 函數改成 apply 函數,能夠簡化調用

  • 使用 context bound 優化隱式參數,別慌,context bound 實際就是個語法糖而已

object Same {
  def apply[T: Eq](a: T, b: T): Boolean = implicitly[Eq[T]].eq(a, b)
}

// 使用 apply 做爲函數, 調用時能夠不用寫函數名
Same(1, 1)
Same("hello", "world")
複製代碼

簡單說一下 context bund,首先泛型的定義 由 T 變成了 [T: Eq],這樣就能夠用 implicitly[Eq[T]] 讓編譯器在做用域內找到一個 Eq[T] 的隱式實例,context bound 可讓函數的簽名更加簡潔。

在 Scala 中,類型類的設計其實隨處可見,典型的就有 Ordered

回望 Java

以判等問題引出 Type classes 有一些不足,咱們只意識到了與 OOP 的繼承是一個不同的判等解決方案,不妨再回到 Java 作一些其餘的比較。

Comparator[T] 接口爲例,在 Java 中咱們常常在集合框架中這樣使用

List<Integer> list = new ArrayList<>();
list.sort(Comparator.naturalOrder())
複製代碼

若是將其改形成爲 Type classes 的話

trait Comparator[T] {
  def compare(o1: T, o2: T): Int
}

object Instances { 
  implicit val intComprator = new Comparator[T] {
    def compare(o1: Int, o2: Int) = o1.compareTo(o2)
  }
  
  //... other instances
}
複製代碼

List 的 sort 方法也須要改成帶隱式參數的方法前面,這樣咱們就不須要顯示的傳 Compartor 實例了

// 編譯期會自動找到 Comparator[Integer] 實例
List[Integer] list = new ArrayList<>();
list.sort()
複製代碼

能夠認爲上面的 Type classes 是基於 Scala 語法的僞代碼

相信你也看出來了,與 Type classes 方案相比,最大的差異就是 Java 須要手動傳入 Comparator 實例,也許你會疑惑:就這?

不要小看這二者的區別,這二者的區別就像用 var 定義類型同樣

// Java8
Map<String, String> map2 = new HashMap<>();

// Java10
var map = new HashMap<String, String>();
複製代碼

若是類型系統能幫你完成的事情,就讓它幫你作吧!

總結一下

看了 Haskell 和 Scala 的例子,最後仍是得總結一下:

Type classes 就是抽象了某一些類型的共同行爲,當某個類型須要用到這些行爲時,由類型系統去找到這些行爲的具體實現。

未完待續

最後仍是得再安利一下 Scala3,在 Scala3 中, Type classes 獲得了足夠的重視,直接提供了語法層面的支持,不再用寫一大堆的模板代碼, 今後能夠叫作 Type classes without Pattern

不過爲了不「長篇大論」,相關的內容就留給下一篇文章了(點贊點贊點贊)。

弱弱的皮一下,還學得動嗎?

參考

  1. 《How to make ad-hoc polymorphism less ad hoc》
  2. Of Scala Type Classes
  3. Where does Scala look for implicits?
  4. Scala 隱式參數
  5. Type classes for the Java Engineer
  6. OOP vs type classes
  7. Cats: Type classes
相關文章
相關標籤/搜索