Subtyping vs Typeclasses(二)

本文由 Yison 發表在 ScalaCool 團隊博客。html

本文咱們將介紹 Type Classes,相似 上一篇文章 說起的 Subtyping ,這也是一種實現多態的技術,然而卻更靈活。java

什麼是 Type Classes

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 的朋友天然聯想到了 ComparableComparator 接口。

Comparable 方案

咱們先來看下 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 方案

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 方案

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 來優化它。

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 這兩種技術作進一步的分析比較。

參考

相關文章
相關標籤/搜索