做爲一個 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
但是對於純函數式的語言,好比 Haskell 來講,它沒有 OOP 中的繼承、類等概念,它又該如何優雅的解決判等的問題呢?ide
若是你以爲 Haskell 比較陌生,咱們就換一種提問的方式:還有其它通用的設計方案能夠解決這類判等問題嗎?函數
固然有,而 Type classes 就是這個領域內最靚的那個仔,要了解 Type classes, 還得先從多態開始。oop
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 的開篇做。
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
複製代碼
在調用 ==
或 /=
函數時,編譯器會根據參數類型自動找到類型類實例,而後調用類型類實例的函數執行調用。
若是用戶須要自定義判等函數,只須要實現本身的類型類實例便可。
此時你可能會不自覺的和最開始提到的繼承方案作一個對比,我畫了兩個圖,能夠參考一下
若是僅僅從結構上來看的話,它們之間的差異就像 Comparable
和 Comparator
同樣。
目前的 Java 是沒法實現 Type classes 的,但同爲 JVM 的語言,多範式的 Scala 卻能夠實現。
與 Haskell 不同, Type classes 在 Scala 中並非一等公民,也就是沒有直接的語法支持,但藉助於強大的隱式系統咱們也能實現 Type classes,因爲實現的步驟比較公式化,也就被稱之爲 Type classes Pattern (類型類模式)。
在 Scala 中實現 Type classes Pattern 大體分爲 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 採用了不一樣的構造方式
兩個實例都被 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
。
以判等問題引出 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。
不過爲了不「長篇大論」,相關的內容就留給下一篇文章了(點贊點贊點贊)。
弱弱的皮一下,還學得動嗎?