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.
TypeClass
是 Haskell
语言里的概念,根据《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
加法半群,根据 Scala
中 implicit
隐式的实现,会在伴生对象中
自动查找隐式实例,所以一般讲实例定义在 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
方法、语法结构的类和隐式转换方法,
这对于所有的类型类都一样,都要写这些重复的代码。去掉这些样板代码,我们真正需要的是:
-
TypeClass
的定义 -
TypeClass
的类型实例 -
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
参考资料
- Simulacrum: https://github.com/mpilquist/simulacrum
- TypeClass in Cats: http://typelevel.org/cats/typeclasses.html
- About Scala Macro: http://docs.scala-lang.org/overviews/macros/overview.html