kun-song / Functional-Programming-in-Scala-Specialization

https://www.coursera.org/specializations/scala
0 stars 0 forks source link

Type Class #4

Open kun-song opened 6 years ago

kun-song commented 6 years ago

基础

在 Scala 中,type class 即为 trait,使用 type class 一般需要:

  1. 创建该 trait 的实现,称之为 type class instance
  2. 将这些 type class instance 标注为 implicit

Scala 中的 Ordering 特质即为 type class,其使用方式如下:

// 创建 type class instance
val minOrdering = Ordering.fromLessThan[Int](_ < _)
val maxOrdering = Ordering.fromLessThan[Int](_ > _)

val xs = List(3, 4, 2)

xs.sorted(maxOrdering)
xs.sorted(minOrdering)

为了避免每次都要手动给 sorted 函数传递参数,可以将常用的 Ordering 实例声明为 implicit

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

val xs = List(3, 4, 2)

xs.sorted  // 4, 3, 2
xs.sorted(minOrdering)  // 2, 3, 4

但同一作用域内,同一类型只能有一个 implicit 实例,否则 Scala 编译器无法选择:

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

val xs = List(3, 4, 2)

xs.sorted
xs.sorted(minOrdering)

上面的例子,将报如下错误:

Error:(6, 5) ambiguous implicit values:
 both lazy value maxOrdering in class A$A252 of type => scala.math.Ordering[Int]
 and lazy value minOrdering in class A$A252 of type => scala.math.Ordering[Int]
 match expected type scala.math.Ordering[Int]
xs.sorted
   ^

可以实现绝对值排序:

implicit val minOrdering = Ordering.fromLessThan[Int]{
  (a, b) ⇒ Math.abs(a) < Math.abs(b)
}

List(3, -4, 2).sorted  // 2, 3, -4

也可以为自定义类型实现 Ordering 实例:

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

implicit val rationalOrdering: Ordering[Rational] = Ordering.fromLessThan[Rational]{
  (a, b) ⇒ a.numerator * b.denominator < b.numerator * a.denominator
}

// List(Rational(1,3), Rational(1,2), Rational(3,4))
List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted

可以看到 Ordering 特质实现了排序函数,但未指定具体类型应该如何排序,实际使用时,需要程序员自己去定义具体类型的排序规则,排序规则定义好后,即可享受 Ordering 特质支持的其他功能,比如例子中的 sorted 方法,所以 type class 是一种高度抽象。

kun-song commented 6 years ago

implicit 作用域

Scala 编译器从 implicit 作用中查找需要的 implicit 值,而 implicit 作用域由多部分组成,不同部分优先级也不同:

打包第一原则

前面我们为 Rational 创建了 Ordering 对象,借助伴生对象,可以提升其可读性,首先,最直接的版本是将 Ordering[Rational] 定义在局部作用域中:

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

object Demo {

  def demo = {
    implicit val rationalOrdering: Ordering[Rational] = Ordering.fromLessThan[Rational]{
      (a, b) ⇒ a.numerator * b.denominator < b.numerator * a.denominator
    }

    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
      List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
  }

}

这当然没有问题,完全可以,但我们不想把 implicit 值放在局部作用域中,我们想要把它移出 Demo 类,由于 implicit 值只能位于 class object trait 等内部,所以可以将其移动到另外一个 object 内部:

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

object Instance {
  implicit val rationalOrdering: Ordering[Rational] = Ordering.fromLessThan[Rational]{
    (a, b) ⇒ a.numerator * b.denominator < b.numerator * a.denominator
  }
}

object Demo extends App {

  def demo = {
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
      List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
  }

  demo

}

但运行该例子,将会报如下错误:

Error:(14, 65) No implicit Ordering defined for com.huawei.archive.demos.Rational.
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==

此时,编译器无法为 sorted 方法找到合适的 Ordering 对象吗,换句话说,定义在 Instance 中的 rationalOrderingsorted 不可见,将 rationalOrdering 放到 Rational 的伴生对象中即可解决该问题:

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

object Rational {
  implicit val rationalOrdering: Ordering[Rational] = Ordering.fromLessThan[Rational]{
    (a, b) ⇒ a.numerator * b.denominator < b.numerator * a.denominator
  }
}

object Demo extends App {

  def demo = {
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
      List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
  }

  demo

}

这里引出打包 type class instance 的第一条原则:

When defining a type class instance, if

  • there is a single instance for the type; and
  • you can edit the code for the type that you are defining the instance for

then define the type class instance in the companion object of the type.

打包第二原则

打开 Ordering 伴生对象的源码,可以看到里面已经预定义了很多 type class instance,例如 Ordering[Int],而前面我们也定义了自己的 Ordering[Int],而且并没有引起编译错误,这说明不同 implicit 作用域之间有优先级。

完整的 implicit 优先级非常复杂,但有一条非常符合直觉的规则:local scope 优先于 companion object scope,即用户在局部作用域中:

优先级都高于伴生对象中的 type class instance。

这引出了打包 type class instance 的第二个原则:

When defining a type class instance, if

  • there is a single good default instance for the type; and
  • you can edit the code for the type that you are defining the instance for

then define the type class instance in the companion object of the type. This allows users to override the instance by defining one in the local scope whilst still providing sensible default behaviour.

打包第三原则

如果 Rational 类型没有一个合适的默认 object Rational,或者可能有多个时,把 implicit 放到 Rational 的对生对象中合适吗?当然不合适!

此时可以将每个 type class instance 都放在自己单独的对象中,例如:

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)
  )
}

用户使用时,手动 import 想要用的 type class instance 即可。

kun-song commented 6 years ago

练习

使用 Order 模拟订单,其中 units 为数量,unitPrice 为单价:

final case class Order(units: Int, unitPrice: Double) {
  val total: Double = unitPrice * units
}

分别为 Order 实现按照 total units unitPrice 三种方式排序:

object Order {
  implicit val totalOrdering: Ordering[Order] = Ordering.fromLessThan(_.total < _.total)
}

object UnitsOrder {
  implicit val unitsOrdering: Ordering[Order] = Ordering.fromLessThan(_.units < _.units)
}

object PriceOrder {
  implicit val priceOrdering: Ordering[Order] = Ordering.fromLessThan(_.unitPrice < _.unitPrice)
}

object Demo extends App {

  import PriceOrder.priceOrdering

  val xs = List(Order(1, 1.0), Order(1, 2.0), Order(1, 1.5), Order(2, 0.2))

  val r1 = xs.sorted

  println(r1)
}