Essential Scala: Type Classes

类型类是Scala中一个非常强的特性,它支持我们在原有类库中使用新的功能,同时不用使用继承或者访问类库源代码.这一节中我们将介绍如何实现类类型,使用Scala中一个叫做”隐式转换”的特性.

类型类给了我们一个第三方实现技术,并且比其他技术都要灵活.一个类型类就像一个接口,使用类型类我们可以做到:

  1. 给一个已有的类提供不同的接口实现
  2. 不修改原有代码的情况下实现一个接口

这意味着我们在不修改原有代码的基础上添加新的方法或数据结构.

Type Class Instances

Ordering

类型类一个简单是例子就是Odering特质.对于类型A,Odering[A]定义了一个比较方法用于比较A的两个实例,构造Ordering的时候,我们可以方便的使用fromLessThan方法定义在伴生对象中.

如果我们想要排序一个整形数字列表,有很多种方法可以使用.比如从高到低排序,或从低到高.List有一个sorted方法用于排序, 但是使用时我们需要提供一个特殊的Odering以便按照我们的要求进行排序.

import scala.math.Ordering

val minOrdering = Ordering.fromLessThan[Int](_ < _)
// minOrdering: scala.math.Ordering[Int] = scala.math.Ordering$$anon$9@787f32b7

val maxOrdering = Ordering.fromLessThan[Int](_ > _)
// maxOrdering: scala.math.Ordering[Int] = scala.math.Ordering$$anon$9@4bf324f9

List(3, 4, 2).sorted(minOrdering) // res: List[Int] = List(2, 3, 4)
List(3, 4, 2).sorted(maxOrdering) // res: List[Int] = List(4, 3, 2)

这里我们定义了两个Odering,minOdering从低到高排序,maxOdering从高到低排序,当我们调用sorted时根据需要传入对应的Odering,这种类型类的实现方式叫做type class instances.

Implicit Values

这种将类型类实例传递给方法调用的方式在我们想要重复使用一个实例时非常不便.Scala提供一个方便叫做implicit value,它通过编译器帮助我们传递类型类实例:

implicit val ordering = Ordering.fromLessThan[Int](_ < _) 

scala> List(2, 4, 3).sorted
// res: List[Int] = List(2, 3, 4)
List(1, 7 ,5).sorted
// res: List[Int] = List(1, 5, 7)

上面代码中我们并没有给sorted方法提供Odering,是编译器帮我们做了.

我们需要告诉编译器需要将哪个值传给sorted方法.我们可以将一个值注释为implicit,同时该方法必须能够接收隐式值.可以查看sorted方法实现文档.

Declaring Implicit Values

我们可以使用implicit标记一个 val/var/object或无参的def,使其成为一个潜在的候选隐式参数.

implicit val exampleOne = ... 
implicit var exampleTwo = ... 
implicit object exampleThree = ... 
implicit def exampleFour = ...

一个隐式值的声明必须包含在一个object, class 或 trait内部.

Implicit Value Ambiguity

如果一个作用域内有多个隐式值会出现什么情况:

implicit val minOrdering = Ordering.fromLessThan[Int](_ < _) 
implicit val maxOrdering = Ordering.fromLessThan[Int](_ > _) 

List(3,4,5).sorted
<console>:12: error: ambiguous implicit values:
both value ordering of type => scala.math.Ordering[Int] 
and value minOrdering of type => scala.math.Ordering[Int] 
    match expected type scala.math.Ordering[Int]
        List(3,4,5).sorted
                           ^

如果使用模糊的隐式值,编译器将会报错.

Organising Type Class Instances

Implicit Scope

当需要一个隐式参数时,编译器会在隐式作用域内寻找合适的隐式值. 隐式作用域有几个部分构成,并且在验证时有优先规则.

隐式作用域的第一部分是普通作用域,这些包括声明在当前作用域的标示符,包括在class,trait,object内部的,或者通过import导入的.一个符合的隐式值必须必须是一个单独的标示符.这个作用域叫做本地作用域.

隐式作用域还包含在方法调用中涉及的类型伴生对象的隐式参数.

sorted[B >: A](implicit ord: math.Ordering[B]): List[A]

编译器会在以下位置查找Ordering实例:

  1. List的伴生对象
  2. Odering的伴生对象
  3. 类型B的伴生对象,即列表中元素的类型或任何父类型

以上显示,我们可以在类型(上面例子中的类型A)的伴生对象中定义类型类的实例,即便不用明确import导入也可以被编译器找到.

下面让我们使用伴生对象来更方便的使用Ordering,首先在本地作用域定义Odering:

final case class Rational(numerator: Int, denominator: Int)

object Example { 
  def example = {
    implicit val ordering = Ordering.fromLessThan[Rational]((x, y) => 
      (x.numerator.toDouble / x.denominator.toDouble) < (y.numerator.toDouble / y.denominator.toDouble)
    )
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted == List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
  } 
}

上面的代码运行的跟预期一致,但是当我们把类型类实例移出本地作用域时却不能成功编译:

final case class Rational(numerator: Int, denominator: Int)
object Instance {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) < (y.numerator.toDouble / y.denominator.toDouble) )
}
object Example { 
  def example =
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted == List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
}

No implicit Ordering defined for Rational.
 assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
 ^

报错找不到需要的隐式值Odering.

但是当我们把类型类实例放到Rational的伴生对象时又更顺利编译了.

final case class Rational(numerator: Int, denominator: Int)
object Rational {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) < (y.numerator.toDouble / y.denominator.toDouble) )
}
object Example { 
  def example =
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted == List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
}

这是打包类型类实例的第一个模式:伴生对象.当我们在定义一个类型类实例时,如果:

  1. 这是这个类型的单例
  2. 你能够编辑这个实例的类型

这时就可以在该类型的伴生对象中定义类型类的实例.

Implicit Priority

如果我们查看Ordering的伴生对象,会发现已经定义了一些类型类实例.当然已经有了一个Int的实例,但是我们仍然能够定义自己的Ordering[Int]而不会因为隐式模糊值问题而报错.

想要理解这里面的原因我们需要学习隐式值的选择优先级.

隐式模糊问题只有在多个类型类实例拥有同样的优先级时出现.否则会选择优先级最高的.

完整的有限级规则十分复杂,但是复杂的优先级对使用比较频繁的用例影响很小.比较常见的是,本地作用域总比伴生对象作用域的优先级要高,意思就是,被明确导入本地作用域的会有限选择,比如定义在本地或在本地作用域明确import.

Packaging Implicit Values Without Companion Objects

如果该类型没有很好的实例或者已经有了几个很好的实例,这是我们不能覆盖任何其中一个,这时比较好的做法就是为每个新的实例创建单独的伴生对象:

final case class Rational(numerator: Int, denominator: Int)

object RationalLessThanOrdering {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
   (x.numerator.toDouble / x.denominator.toDouble) < (y.numerator.toDouble / y.denominator.toDouble) )
}

object RationalGreaterThanOrdering {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
  (x.numerator.toDouble / x.denominator.toDouble) > (y.numerator.toDouble / y.denominator.toDouble) )
}

在使用的时候就可以按需进行导入指定的实例.

Creating Type Classes

Elements of Type Classes

类型类模式有四个部分:

  1. class本身实际的type
  2. type class实例
  3. 使用隐式参数的interfaces
  4. 使用enrichment和隐式参数的interfaces

Creating a Type Class

让我们新建一个例子:将数据转换成HTML.

一种实现是创建一个trait,需要使用该功能的地方进行混入就行了:

trait HtmlWriteable { 
    def toHtml: String
}
final case class Person(name: String, email: String) extends HtmlWriteable { 
    def toHtml = s"<span>$name &lt;$email&gt;</span>"
}
Person("John", "john@example.com").toHtml
// res: String = <span>John &lt;john@example.com&gt;</span>

这种实现方式有很多弊端.首先我们被限制只有一种方式来渲染Person.比如我们想把用户列表在公司首页展示,但并不是对所有人都展示email,比如只对已登录的用户展示email.第二,我们只能把这种功能添加到我们自己定义的类上.如果我想把java.util.Date转换成HTML,就不得不写一些别的功能来支持转换.

或许我们可以尝试模式匹配:

object HtmlWriter {
  def write(in: Any): String = in match {
    case Person(name, email) => ...
    case Date => ...
    case _ => throw new Exception(s"Can't render ${in} to HTML")
  } 
}

这种实现也有他自己的问题,我们只能对那些已经列出的类型进行转换,如果需要转换一个新的类型则需要修改代码.

这时我们可以把这个功能改写成一个适配器class:

trait HtmlWriter[A] {
    def write(in: A): String
}
object PersonWriter extends HtmlWriter[Person] {
    def write(person: Person) = s"<span>${person.name} &lt;${person.email}&gt;</span>"
}
PersonWriter.write(Person("John", "john@example.com"))
// res: String = <span>John &lt;john@example.com&gt;</span>

这样就好多了,我们可以为其他类型实现对应的HtmlWriter,包括哪些并不是我们自己定义的类:

import java.util.Date
object DateWriter extends HtmlWriter[Date] {
    def write(in: Date) = s"<span>${in.toString}</span>"
}
DateWriter.write(new Date)
// res: String = <span>Sat Apr 05 16:01:58 BST 2014</span>

或者另外写一个HtmlWriter以便人员信息在主页上展示:

object ObfuscatedPersonWriter extends HtmlWriter[Person] { 
    def write(person: Person) = 
        s"<span>${person.name} (${person.email.replaceAll("@", " at ")})</span>" }
ObfuscatedPersonWriter.write(Person("John", "john@example.com")) 
// res: String = John (john at example.com)

Implicit Parameter and Interfaces

上面的模式也有问题,如果我们要处理复杂数据结构时需要管理很多HtmlWriter实例.

Implicit Parameter Lists

下面是一个隐式参数的例子:

object HtmlUtil {
    def htmlify[A](data: A)(implicit writer: HtmlWriter[A]): String = {
        writer.write(data)
    }
}

htmlify方法需要两个参数,需要转换的数据和作为隐式参数的writer,关键字implicit会把隐式参数添加到整个参数列表而不是作为一个单独的参数,但是这个参数在方法调用时作为可选项,我们可以按照正常的方式进行调用:

HtmlUtil.htmlify(Person("John", "john@example.com"))(PersonWriter) 
// res: String = <span>John &lt;john@example.com&gt;</span>

或者省略掉该隐式参数,如果省略,编译器会去搜索正确类型的隐式值作为参数对省略的位置进行填充.首先我们定义一个隐式值:

mplicit object ApproximationWriter extends HtmlWriter[Int] { 
  def write(in: Int): String =
    s"It's definitely less than ${((in / 10) + 1) * 10}"
}

然后当我们在调用HtmlUtil时就不需要再指定隐式参数了:

HtmlUtil.htmlify(2)
// res: String = It's definitely less than 10

Interfaces Using Implicit Parameters

object HtmlWriter {
  def write[A](in: A)(implicit writer: HtmlWriter[A]): String =
    writer.write(in)
}

我们可以用下面的结构避免上面的这种写法:

object HtmlWriter {
  def apply[A](implicit writer: HtmlWriter[A]): HtmlWriter[A] =
    writer 
}

然后按下面的形式进行调用:

HtmlWriter[Person].write(Person("Noel", "noel@example.org"))

Type Class Interface Pattern:

object TypeClass {
  def apply[A](implicit instance: TypeClass[A]): TypeClass[A] =
    instance 
}