Scala 中 Type Class 实现的套路

typeclass.png

Scala 中 Type Class 实现的套路

什么是Type Class

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. A lot of people
coming from OOP get confused by typeclasses because they think they are like classes in object
oriented languages. Well, they're not. You can think of them kind of as Java interfaces, only better.

TypeClassHaskell 语言里的概念,根据《Learn you a haskell》中的解释, TypeClass 是定义一些公共行
为的接口,=Java= 语言中最为接近的概念是 interface, Scala 语言中最为接近的概念是 trait.

Haskell 中,可以给任意类型实现任意 TypeClass, 而在 Java 中只能通过实现某个接口才能让 Class 具有接口定义
的行为, 如果要给某个类库的类型添加额外的行为,几乎是不可能的事情(通过代码生成技术可以).

为什么需要Type Class

TypeClass行为定义具有行为的对象 分离,更容易实现 DuckType; 同时, 在函数式编程中,通常将数据与行为
相分离,甚至是数据与行为按需绑定,已达到更为高级的组合特性.

Scala 中对 TypeClass的实现<a id="sec-1-3" name="sec-1-3"></a>

Scala 是基于JVM平台的语言,受JVM的限制,不具备像 Haskell 那样语言原生对 TypeClass 的支持, Scala 语言使用了
隐式转换的魔法,使之能够不那么完美地支持 TypeClass.

Scala 中实现 TypeClass 可总结为三板斧.

1. TypeClass Trait 定义

TypeClass定义即使用 trait 来定义相关行为接口,比如 半群 :

trait Semigroup[A] {
  def combine(a1: A, a2: A): A
}

2. 定义 TypeClass 隐式实例

针对需要实现 TypeClass 的隐式实例,比如我们实现 Int 加法半群,根据 Scalaimplicit 隐式的实现,会在伴生对象中
自动查找隐式实例,所以一般讲实例定义在 TypeClass 的伴生对象中,这样只要 import TypeClass 就可以获得隐式实例:

object Semigroup {
  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }
}

一般地,还会在伴生对象中增加 apply 只能构造器来生成 TypeClass 实例,而 apply 方法可以省略,代码看起来会更加简洁:

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }
}

这样,我们就可以通过伴生对象,获取 TypeClass 实例,从而调用 TypeClass 的行为方法:

import Semigroup._

Semigroup[Int].combine(1, 2) // 3

3. 定义 TypeClass 语法结构

为了进一步简化代码,我们可以定义一些语法结构,来隐藏 TypeClass 的样板式的代码,如:
Semigroup[Int].combine(1, 2) , 我们希望通过简单的表达式来表示: 1 |+| 2. 如何来
实现呢?

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }

  // 增加语法结构
  abstract class Syntax[A](implicit I: Semigroup[A]) {
    def a1: A
    def |+|(a2: A): A = I.combine(a1, a2)
  }

  // 定义隐式转换方法,将
  implicit def to[A](target: A)(implicit I: Semigroup[A]): Syntax[A] = new Syntax[A] {
    val a1 = target
  }

}

定义完成后,我们就可以使用新的语法 |+| 来调用 Semigroup 这个 TypeClass 的方法了:

import Semigroup._

Semigroup[Int].combine(1, 2) // 3

// 使用语法操作符
1 |+| 2 // 3

改进,更简洁

将上述实现综合一下,实现一个 Semigroup TypeClass 需要以下代码:

trait Semigroup[A] {
  def combine(a1: A, a2: A): A
}

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }

  // 增加语法结构
  abstract class Syntax[A](implicit I: Semigroup[A]) {
    def a1: A
    def |+|(a2: A): A = I.combine(a1, a2)
  }

  // 定义隐式转换方法,将
  implicit def to[A](target: A)(implicit I: Semigroup[A]): Syntax[A] = 
    new Syntax[A] {
      val a1 = target
    }

}

仔细分析,这里面有一些样板代码,比如伴生对象的 apply 方法、语法结构的类和隐式转换方法,
这对于所有的类型类都一样,都要写这些重复的代码。去掉这些样板代码,我们真正需要的是:

  1. TypeClass 的定义
  2. TypeClass 的类型实例
  3. TypeClass 的语法操作符

得益于 Scala 元编程的强大,我们可以通过 Macro 来自动生成这些样板代码。 Simulacrum
项目就是专注于此。

利用 Simulacrum, 我们可以将我们的代码改进为

import simulacrum._

@typeclass trait Semigroup[A] {
  @op("|+|") def combine(a1: A, a2: A): A
}

// 定义隐式实例
implicit val intPlusInstance = new Semigroup[Int] {
  def combine(a1: Int, a2: Int): Int = a1 + a2
}

// 使用
import Semigroup.ops._
1 |+| 2 // 3

参考资料

  1. Simulacrum: https://github.com/mpilquist/simulacrum
  2. TypeClass in Cats: http://typelevel.org/cats/typeclasses.html
  3. About Scala Macro: http://docs.scala-lang.org/overviews/macros/overview.html

推荐阅读更多精彩内容

  • Scala中的implicit关键字对于我们初学者像是一个谜一样的存在,一边惊讶于代码的简洁,一边像在迷宫里打转一...
    大刀阅读 12,661评论 10 18
  • 这篇讲义只讲scala的简单使用,目的是使各位新来的同事能够首先看懂程序,因为 scala 有的语法对于之前使用习...
    MrRobot阅读 2,233评论 0 10
  • 除了在 Predef 对象中自动加载的那些隐式对象外,其他在源码中出现的隐式对象均不是本地对象。[P112] 隐式...
    云之外阅读 442评论 0 0
  • 戏路如流水,从始至终,点滴不漏。一路百折千回,本性未变,终归大海。一步一戏,一转身一变脸,扑朔迷离。真心自然流露,...
    刘光聪阅读 1,505评论 3 13
  • Adora Cheung, Homejoy的创始人你该成为你所在行业的行家找到真正的痛点,一句话描述它,并让自己的...
    LeaChau阅读 151评论 0 2