本文由 Yison 發表在 ScalaCool 團隊博客。html
本文咱們將介紹 Type Classes,相似 上一篇文章 說起的 Subtyping ,這也是一種實現多態的技術,然而卻更靈活。java
Type Classes 是發源於 Haskell 的一個概念。顧名思義,很多人把它理解成 「class of types」,這其實並不科學。事實上,Haskell 並無相似 Java 中的 class 的概念,一個更準確的理解,能夠是「constructor class」 — 本質上它區別於單態,但也不是多態,而是提供一個介於二者之間的過渡機制。算法
讓咱們看看 《Learn You a Haskell for Great Good! 》 中對 Type classes 的相關描述:安全
A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes.ide
簡單理解,咱們能夠基於一個 type class 創造不一樣的類型,來實現多態的需求。wordpress
接下來咱們將經過具體的例子來進一步認識 type classes,目前,你可能仍然不明白,但你能夠把它想象爲相似於 Java 中的 Interfaces,雖然這也不許確。優化
想象咱們如今要爲某兩款 Moba 遊戲(G1 和 G2)寫段程序,支持在有限的玩家中篩選出 MVP 選手。spa
假設兩遊戲在評價 MVP 中對 KDA 中的助攻指標權重不一樣, 公式以下:scala
MVP (G1) = (人頭數 + 助攻數 x 0.8) / 死亡數 MVP (G2) = (人頭數 + 助攻數 x 0.6) / 死亡數code
case class Player1(kill: Int, death: Int, assist: Int) = {
def score = (kill + assist * 0.8) / death
}
case class Player2(kill: Int, death: Int, assist: Int) = {
def score = (kill + assist * 0.6) / death
}
複製代碼
有經驗的朋友很快發現這實際上是一個排序問題,又熟悉 Java 的朋友天然聯想到了 Comparable
和 Comparator
接口。
咱們先來看下 Comparable
接口的定義:
public interface Comparable<T> {
int compareTo(T o) } 複製代碼
很是簡單,內部只定義一個 compareTo
方法,實現接口的類能夠自定義該方法的實現,由此對具體的類型比較大小。
Scala 兼容 Java 的類庫,因此咱們能夠這樣實現:
case class Player1(kill: Int, death: Int, assist: Int) extends Comparable[Player1] = {
def score = (kill + assist * 0.8) / death
// 覆寫 compareTo
override def compareTo(o: Player1): Int = java.lang.Long.compare(score, o.score)
}
case class Player2(kill: Int, death: Int, assist: Int) extends Comparable[Player2] = {
def score = (kill + assist * 0.8) / death
// 覆寫 compareTo
override def compareTo(o: Player2): Int = java.lang.Long.compare(score, o.score)
}
複製代碼
在 Java 中,這是對排序問題很標準的一種處理方式,它的優勢顯而易見 — 只需定義一次,則能夠在任何有 PlayerX
的地方進行 compare。然而它的缺點也一樣明顯,若是我想要在不一樣的地方對 PlayerX
採用其它的排序算法,那麼就有點捉襟見肘了。
此外,該種方式還有個較大的問題,它並非「類型安全」的,須要額外的處理,相似的緣由咱們會在後續的文章中做更深刻的介紹。
Comparator
相比 Comparable
要靈活一些,這實際上是一種很常見的思路。咱們先在 Scala 中如此實現:
val players = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
players.sortWith((p1, p2) => p1.score >= p2.score).head
複製代碼
顯然它能夠在調用處隨意定義排序算法,然而卻又增長了每次調用時定義算法的成本。
好吧,咱們仍是須要模擬一個 Comparator
接口:
trait Comparator[T] {
def compare(first: T, second: T): Int
def >=(first: T, second: T): Boolean = compare(first, second) >= 0
}
object G1 {
def ordering(o: (Player1, Player1) => Int) = new Comparator[Player1] {
def compare(first: Player1, second: Player1) = o(first, second)
}
val mvp = ordering((p1: Player1, p2: Player1) => (p1.score - p2.score).toInt)
}
object G2 {
def ordering(o: (Player2, Player2) => Int) = new Comparator[Player2] {
def compare(first: Player2, second: Player2) = o(first, second)
}
val mvp = ordering((p1: Player2, p2: Player2) => (p1.score - p2.score).toInt)
}
複製代碼
大功告成,咱們對樣板數據篩選 MVP:
def findMvp[T](list: List[T])(ordering: Comparator[T]): T = {
list.reduce((a, b) => if (ordering >=(a, b)) a else b)
}
val players1 = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
findMvp(players1)(G1.mvp)
val players2 = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
findMvp(players2)(G2.mvp)
複製代碼
看起來不錯,美中不足是每次調用 findMvp
時都必須顯式地指定排序算法。
Type Class 能夠很好地解決以上的幾個問題。在 Scala 中,類型系統其實並無像 Haskell 同樣內置 Type Class 原生特性,不過咱們能夠經過 implicit
來實現所謂的 Type Class Pattern,所以反而更增強大。
相比 Haskell,Scala 中的 Type Class Pattern 能夠對不一樣的做用域採起選擇性生效,可參見 Scala Implicits : Type Classes Here I Come
首先,咱們先來改造下 findMvp
:
def findMvp[T](list: List[T])(implicit ordering: Comparator[T]): T = {
list.reduce((a, b) => if (ordering >=(a, b)) a else b)
}
複製代碼
緊接着,再給咱們的排序算法定義增長 implicit
:
object G1 {
def ordering(o: (Player1, Player1) => Int) = new Comparator[Player1] {
def compare(first: Player1, second: Player1) = o(first, second)
}
implicit val mvp = ordering(_.score - _.score)
}
object G2 {
def ordering(o: (Player2, Player2) => Int) = new Comparator[Player2] {
def compare(first: Player2, second: Player2) = o(first, second)
}
implicit val mvp = ordering(_.score - _.score)
}
複製代碼
而後,咱們就能夠如此調用了:
import G1.mvp
import G2.mvp
val players1 = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
findMvp(players1)
val players2 = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
findMvp(players2)
複製代碼
如此神奇?因爲定義了 implicit ordering
,Scala 編譯器會在 Comparator[T]
特質中自動尋找到相關的 ordering 。
Scala 中的 Type Class 就是如此的簡單,也許你仍是對 findMvp
的定義有點不適,好吧,咱們能夠利用 Context Bounds 來優化它。
這個名字看起來也有點怵,其實它無非只是一種語法糖而已。拿以上的例子來說,[T: Comparator]
就是一個 context bound,它告訴編譯器當 findMvp
被調用時,Comparator[T]
類型的一個 implict 值會存在做用域當中。以後咱們就能夠 implicitly[Comparator[T]]
來獲取這個值。
所以,優化語法後的代碼以下:
def findMvp[T:Comparator](list: List[T]): T = {
list.reduce((a, b) => if (implicitly[Comparator[T]] >=(a, b)) a else b)
}
複製代碼
經過以上的介紹,咱們發現 Type Classes 是一種靈活且強大的技術,Scala 標準庫以及其它不少知名的類庫(如 Cats)都大量使用了這種模式。
它有點相似咱們熟悉的 Interfaces(對應 Scala 中的 Trait),均可以經過名字、輸入、輸出,描述一系列相關的操做。然而,它們又顯著地不一樣,在下一篇文章中,咱們將對 Subtyping 和 Typeclasses 這兩種技術作進一步的分析比較。