本文由 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 这两种技术作进一步的分析比较。