scala 的 Dynamic 与其应用

scala 新版本 2.10 引入了 Dynamic 这个 trait,意思上就是希望能够动态的 handle 一些东西。其实如果用过 ruby 的 ActiveRecord 或者 django ORM 的话,你就会发现这个 feature 对于实现这类数据库应用是非常有用的,相比 Java 的 hibernate,每个都要写一个类表示 model 以后还要写 DAO 这么复杂的,如果能动态的为每个 model 生成合适的方法事情就好办很多了,可以比较一下使用 java 实现的 ActiveRecord 的 pattern

言归正传,因为 Dynamic 是一个 trait,我们可以 mixin 到一个类里面用来解决“未知”的东西,这点有点像 ruby 的 method_missing,即进行 method/member 查询一旦失败了就会 default 到这个 trait 带来的几个方法上:

  • applyDynamic (m: String) (args: Any*) 对应于 obj.do(something) 这种 case
  • selectDynamic (m: String) 对应于 obj.something 这种 case
  • updateDynamic (m: String) (arg: => Any) 对应于 obj.something = ? 这种情况

简单的例子如下:

package demo

import scala.language.dynamics

object DSMap {
  def apply (m: Map[String, String]) = new DSMap (m)
}

class DSMap (m: Map[String, String]) extends Dynamic {
  lazy val caller = m ("caller").toUpperCase

  def selectDynamic (prop: String) = m(prop)
}

这里为 DSMap 实现了 selectDynamic 的方法,这个 domain specific 的 map 的作用在于,如果我们希望从 Map 获得信息,它本身又需要一些 helper object,一种可能的策略就是通过一些 lazy val 存放那些可能会被需要的东西,这些东西都是利用 Map 和额外的 helper object 计算出来的,同时可以直接用 .something 访问 Map 的内容。

package demo

object hello extends App {
  val m = DSMap (Map ("caller" -> "abc", "op" -> "sum"))
  println (m.caller)
  println (m.op)
}

比如你有一组 API,接受的输入都是 Map 他们都会根据这个 map 获得一些信息,有一些是公用的(比如获得 caller 是谁),有一些是 API-specific 的,使用 lazy val 就能避免计算所有的 case,但是又可以避免显式引入一些不必要的 local variable,比如每个 API 尽管都会验证 caller 从中抽去信息,但是他们很可能仅仅是借助 caller 拿到更进一步的属性配置来计算别的东西(如 rules),这时就可以直接在 API 中 val rules = m.rules 而忽略掉中间通过 caller 走过的一系列环节。

很自然的一个想法就是我们是否能利用这个特性来做 proxy/delegate,所谓的 delegate 就是它和目标类/对象有一样的行为(但是他自己并不直接实现,他将这些调用 forward 到目标对象/类),同时允许你实现一些自己的方法。实际上有人试图实现这样一个 pattern 了,

trait Delegate[T] extends Dynamic {
  var delegate: T = _

  import scala.reflect.runtime.{universe => ru}
  def applyDynamic(method: String)(args: Any*)(implicit tTag: TypeTag[T], cTag: ClassTag[T]) = {
    val m = ru.runtimeMirror(delegate.getClass.getClassLoader)
    val sym = ru.typeTag[T].tpe.declaration(ru.newTermName(method)).asMethod
    val im = m.reflect(delegate)
    val methodMirror = im.reflectMethod(sym)
    methodMirror.apply(args:_*)
  }
}

这么短的一段代码却非常难以看懂,部分原因是因为 scala 使用了不同于 Java 的反射机制实现。其实反射(reflection)这个概念着实很难理解清楚其内涵,也许大家看见从字符串转换变成 Class 然后使用 newInstance 获得实例就知道这是段使用了 reflection 的代码,但是总觉得这只是一个“外延”,是一个 reflection 的例子,可是为什么要叫 reflection 而不叫什么字符串转换到类的方法呢?我现在一个粗浅的理解是 reflection 代表的是“程序的另一面”:如果说大部分程序员写的不依赖 reflection 的 code 是“实体”,使用 reflection 写程序可以认为是在“虚像”上面编程(reflection 就是一组提供给程序员在虚像上编程的 API)。我们说 Foo foo = new Foo () ; 这句话就是编程语言本身的 feature 告诉我们的,初始化一个对象,非常的具体,它里面通行的语言就是我们的程序语言;另一个世界里面(很可能是编译器、解释器眼中的世界里面)可能只是一个 AST,它最后把这个结构 bind 到了初始化一个对象的行为上,(对于编译器来说它本身其实也是将字符串映射到某些行为,但是这个行为的输出是 bytecode 或者机器码),reflection API 其实是在这个“虚幻的世界”里面存在的 API(提供给程序语言世界),比如它提供给你了将字符串转换到实例的能力(对应于直接通过程序语言书写的能力)。

java 提供的反射 API 一般始于对象本身,通过 getClass 然后利用 Class 对象获得方法、成员、annotation 等等。而 scala 提供的反射 API 基于 mirror,我们下篇文章讨论一下这方面的东西。简单的说前面几行代码表示的是从字符串(方法名)转换到具体方法调用行为的实现对象(method mirror)的过程,最后通过反射获得的方法进行了调用(apply)并返回。那么以上 Delegate 类做的事情本质上是在运行时通过反射将给定方法 forward 到 delegate 成员上,它作为一个 trait 提供给用户。

不过有意思的是这个 delegate 并不好用,一些简单的情况可能 okay,比如自己写的非常简单的类,对应的 delegate 很容易 work,并且你可以用 mixin 加入新的东西

class MyClass (a: String, b: Int) {
  // ...
}

trait WithHelper { this: MyClass =>
  val helper: Helper = _
  lazy val foo = anything with helper
  // ...
}

val target = new MyClass ("foo", 1)
val d = new MyClass with Delegate[MyClass] with WithHelper {
  helper = something ;
  delegate = this
}

这样一来 d 可以像 MyClass 一样用,还能用 WithHelper 里面的好东西。不过这个方法对 Map 不适用,原因暂时没想清楚,但是很显然 Map 本身不是实现类,只是一个 trait,另外就算用一个实现类如 HashMap 也很难跟这个 Delegate 合用(出现问题在 tpe.declaration 那里,似乎这只能把一些实现的方法拿到,而来自 traits 等方法似乎拿不到)。基于这个原理,我提供了一个修正的版本如下:

package demo ;

import scala.language.dynamics
import reflect.ClassTag
import scala.reflect.runtime.universe._

object Delegate {
  def apply [T] (o: T) = new Delegate[T] {
    delegate = o
  }
}

trait Delegate[T] extends Dynamic {
  var delegate: T = _

  import scala.reflect.runtime.{universe => ru}
  def applyDynamic(method: String)(args: Any*)(implicit tTag: TypeTag[T], cTag: ClassTag[T]) = {
    val m = ru.runtimeMirror(delegate.getClass.getClassLoader)
    val n = ru.newTermName(method)
    val klass = ru.typeTag[T].tpe.baseClasses.find { k =>
      k.asClass.toType.declaration(n) match {
        case ru.NoSymbol => false
        case _ => true
      }
    } getOrElse {
      throw new java.lang.NoSuchMethodException(s"$method not found!")
    }
    val sym = klass.asClass.toType.declaration(n).asMethod
    val im = m.reflect(delegate)
    val methodMirror = im.reflectMethod(sym)
    methodMirror.apply(args:_*)
  }
}

这样一来你就能很容易的用它来做坏事了

package demo

object hello extends App {
  val tm = Map ("caller" -> "abc", "op" -> "sum")

  val d = Delegate (tm)
  println (d ("caller"))

  val a = new Delegate[Map[String, String]] {
    delegate = tm
    lazy val uuid = "uuid of caller"
  }
  println (a.uuid)
  println (a ("caller"))
}

最后说一句的是,这个 delegation 没有必要发生在 runtime,很自然的我们希望这个可以在 compile-time 解决,这就需要 compile-time reflection 了!什么不知道,那就是 scala 的 macro!

——————
And Rachel said, With great wrestlings have I wrestled with my sister, and I have prevailed: and she called his name Naphtali.

Advertisements
scala 的 Dynamic 与其应用

发表评论

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