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

参考

相关文章
相关标签/搜索