typeclass 与 domain model

typeclass 源自一个比较平实的想法,程序传递的是数据,而并不是“类型”,这个想法跟 OOP 将数据本身和方法绑定在一起是有一些原则上不同的。

在很多问题里面,不同的 layer 对数据需要的方法是很不一样的:设计模式里面常用,特别对 Java 经常见到的一些事情就是将一个类型的数据 copy 到另外一个类型里面去,为什么?因为从一个 domain 进入另一个 domain 以后,专门为某个 domain 设计的对象拥有的方法不能满足另外一个 domain 的需求,因此往往需要将数据从一个对象转移到另一个类型里面。具体的例子是,假设我们使用 hibernate 对数据库建模,我们使用的对象往往需要 JPA 的 annotation,提供的方法往往是几个 model 之间的相互关系;我们 persistence layer 对 service layer 提供的交互往往就是建立在这些 model 之上的;service layer 需要将数据展示给用户,通常或者通过某些 template 或者直接将 hibernate model 里面的部分数据返回到客户端,这需要对数据进行串行化等等,通常 web service 会有自己的一套 WSDL 用来生成自己的 model,于是很多时候你需要一个 DTO 将 hibernate model 转移到 web service 的 model。

与这种想法不同的另一种策略是,数据本身拥有的方法可以尽量简单(比如 getter/setter),进入某个 domain 之后通过 ad-hoc polymorphism 为数据添加 domain specific 的方法。

实际上说到 polymorphism,其实有许许多多不同的 interpretation,

  • 函数重载,一个函数可以具有不同的行为(根据 signature 来区分)
  • 通过继承 override (virtual)函数,一个函数可以对不同的对象具有不同的行为
  • parametric polymorphism,通过 generic type 获得的不同行为,比如 collection 对不同的类型实施的行为(语义上是一致的,但实际的行为是不大一样的)
  • F-bounded polymorphism,这出现在父类需要获得子类的类型的情形,这时的形式往往是 X[A<: X[A]],其中 X 是父类,它具有一个 generic type A,这个类型是本类型 X[A] 的子类,比如实现比较的 trait,这个接口自己是不知道怎么比较的,因此必须对子类实现:这样定义函数接口时可以写 int cmp (a: A),于是子类实现的时候是自己跟自己的比较而不是其他类型(无法比较)
  • ad-hoc polymorphism 是一种对 higher kind of types 的一种行为,通常如果想为几个“看起来相似但实际却没有关系的类型”进行一致的处理就需要 ad-hoc polymorphism,其实这就是类似 C++ 的 traits 做 binding,比如需要为 List、Option、Try 这几个东西提供统一的一套方法,我们可以定义 X[M[_]] 这样一个 generic type,然后将具体的实现通过 implicit val 提供出来,这时我们可以通过 implicitly 获取这些实现从而在一个函数里面为不同的 M 实现不同的行为

那么如何为一个数据添加 domain specific 的行为呢?这当然也是 implicit 的功劳:

  • implicit 的 def 可以提供某个类到另一个类的(隐式)转换,这样可以将后者的方法添加到前者的对象上;类似的有 implicit class extends AnyVal 技巧
  • 通常这个技巧实现了转换本身,但是并没有提供“实现”,通常既然实现是 domain specific 的作为提供给 domain implementer 的 library 就应该提供一些简单的函数用来从这些“实现”生成他们需要的 implicit

这个技巧我们可以通过下面的例子来看,比如为任意类型添加 truthy 返回一个 boolean

trait CanTruthy[A] {
  def truthys (a: A): Boolean
}
object CanTruthy {
  def apply[A] (implicit ev: CanTruthy[A]): CanTruthy[A] = ev
  def truthys[A] (f: A => Boolean): CanTruthy[A] = new Cantruthy {
    def truthys (a: A) = f (a)
  }
}

通过这个 trait 和 companion object 我们可以为 A 创建对应的转换工具,如 CanTruthy.truthys {},下面我们需要提供隐式转换

trait CanTruthyOps[A] {
  def self: A
  implicit def F: CanTruthy[A]
  def truthy: Boolean = F.truthys (self)
}
object ToCanTruthyOps {
  implicit def toCanTruthyOps[A] (a: A) (implicit ev: CanTruthy[A]): CanTruthyOps[A] = new CanTruthyOps {
    def self = a
    implicit def F = ev
  }
}

注意这里的技巧是提供的转换函数本身也带有一个 implicit 参数,这个参数通过前面的 CanTruthy.truthys 生成(实现),而这部分通过 import ToCanTruthyOps._ 我们就能获得一个将 A 转换成为 CanTruthyOps[A] 的对象,这个对象可以使用 truthy。

除了为类型提供额外的方法,我们其实还可以为对应的 companion object 增加新的方法,策略就是为 val f: X.type 建立一个 implicit class,其中 X 是对应的 companion object。

scalaz 提供了一坨 typeclasses,这个意义上来说算是对标准库的一种扩展,而其中的各种 typeclass 因为可以扩展到 domain specific 问题里面,算是一种 open design。比如 scalaz.Applicative 提供了 point 方法,它能将值映射到指定的 monad 类型,如 1.point[Option] 返回的就是 Some(1),如果仔细看看源代码,在 std/Option.scala 里面提供了 point 的实现,如果你使用 Applicative[Option].point (1) 就会得到类似的结果,对应有个 scalaz.syntax.ApplicativeOps 和 ToApplicativeOps 实现了 implicit 转换部分。

——————
But when the cattle were feeble, he put them not in: so the feebler were Laban’s, and the stronger Jacob’s.

Advertisements
typeclass 与 domain model

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s