scala的宏与动态化

作者:liuguobing   阅读 (3457)  |  收藏 (0)  |  点赞 (0)

摘要

昨天在使用scalikejdbc时发现它们用了scala动态特性和宏编程的东西,于是我就有兴趣了解下,并作出总结


原文链接:scala的宏与动态化

scala动态特性
像ruby、groovy等语言都有一种动态(dynamic)的东西(如对象调用它的所述类并未创建的方法),它们属于各自语言元编程的一部分,由于它们本身就是动态弱语言,包含这些东西并不奇怪。scala是静态的强类型语言,而且所有的类型系统在编译时就要确定的,这个特性似乎比较难实现,但sip17提出了加入此功能的建议,scala团队也完成了这个,他们是通过编译器对这些代码重编码,
先直接看个例子吧:
class WrapperMapper[M](map: mutable.Map[String,M]) extends Dynamic{
      def selectDynamic(name:String):Option[M] = map.get(name)
      def updateDynamic(name:String)(value:M): Option[M] = map.put(name,value)
    }
    val i = new WrapperMapper[Int](mutable.Map("love" -> 2))
    println(i.love)
    i.hate = 5
    println(i.hate)
WrapperMapper并没有love和hate属性或方法,但依然可以调用,这是为什么呢?scala的编译器在使用WrapperMapper这个类的对象的地方,如果发现它的对象调用了本不存在的方法、属性等等,就会给它加工一下,让他变成selectDynamic\updateDynamic等等方法,而本来的方法名变成相应的参数,具体的对应策略如下:
*{{{
*  foo.method("blah")      ~~> foo.applyDynamic("method")("blah")
*  foo.method(x = "blah")  ~~> foo.applyDynamicNamed("method")(("x", "blah"))
*  foo.method(x = 1, 2)    ~~> foo.applyDynamicNamed("method")(("x", 1), ("", 2))
*  foo.field           ~~> foo.selectDynamic("field")
*  foo.varia = 10      ~~> foo.updateDynamic("varia")(10)
*  foo.arr(10) = 13    ~~> foo.selectDynamic("arr").update(10, 13)
*  foo.arr(10)         ~~> foo.applyDynamic("arr")(10)
*  }}}
注意,可以动态调用的类必须是继承这个特质:Dynamic,而且必须导入这个变量:scala.language.dynamics,否则编译器会编译出错(或者编译时加上一个参数:-language:dynamics
再来看scala的宏编程(macro),这个才是scala的元编程。
大家都应该用过java的反射,scala是java的衍生语言,自然也有反射,而且它还有一种更高级的反射,就是编译时反射,它就是宏。
宏在scala是实验性产品,尚未纳入标准库中去,但不阻碍我们去使用。不过这个是有点复杂,而且ide容易报错(但实际并没有错),而且编译时有些代码需要分别编译否则会报错,所以上手很容易出错,先来看个例子:
object Debug {
  def apply[T](x: => T):T = macro impl
  def impl(c:blackbox.Context)(x:c.Tree) = {
    import c.universe._
    val q"..$stats" = x
    val loggedStats =  stats.flatMap {stat =>
      val msg = "executing " + showCode(stat)
      List(q"println(${msg})",stat)
    }
    q"..$loggedStats"
  }
}
使用部分:
val n = Debug {
  val a = 1
  val b = a + 2
  a + b
}
println(n)
debug有个apply方法,它的方法体是macro impl,也就是宏指向impl,而impl是个宏方法(我自己命名的),它接受两个参数,第一个是一个blackbox.Context,在2.10版是Context,2.11版有两个,一个是blackbox和whitebox,这两个我不花大篇幅的讲(因为我不太能讲清楚),反正这里都能用,后者用处比前者稍广一些,但scala对前者使用比较有信心,并会先将他放进标准库中。第二个参数是一个c.Tree的,它代表着原方法的参数。
再来看一个神奇的例子:
object TestImpl {
  def _println[T:c.WeakTypeTag](c:blackbox.Context)(cond:c.Tree) = {
    import c.universe._
//    val Literal(Constant(v:Int)) = cond.tree
    //这样写编译时就会打印,而不是运行时
    q"""${println(cond)}"""
  }
}
class PrintA[T] {
  def myPrint(cond:T):Unit = macro TestImpl._println[T]
}
这个是我第一次测试macro写的例子,使用部分:
val printA = new PrintA[Int]
printA.myPrint(2)
当我在编译时我发现编译期间就打印了2,运行时却没有,真是太神奇了,这说明了一点,编译器它先运行了这个宏内的代码,再把q中的结果填到原来使用的该方法的地方去。q叫做“quasiquotes”,很多拥有宏编程的都有这个东东,我对宏编程不算很了解,但这里有一点我是知道的,这个quasiquotes可以取出code,而这里的${}部分会先进行计算在包含到这个code中去,scala编译器再把这个code放到调用的地方去,然后再去编译,c语言也有宏,了解它的应该知道宏可以预编译,然后在汇编时把这个预编译的代码放到调用的源码中,比如#define PI = 3.14,然后在每个用到PI的地方它做了个替换工作,而不是变量引用。scala的宏做了差不多的工作。我们吧使用部分稍作修改下:
val printA = new PrintA[Int]
val x = 2
printA.myPrint(x+2)
在编译是会打印x.+(2),是不是很有意思?这说明这个传进的c.Tree也是个表达式(其实也是个"quasiquotes")。当然这个结果不是我们想要的,所以做点修改,将_println方法改成这样子;
def _println[T:c.WeakTypeTag](c:blackbox.Context)(cond:c.Tree) = {
    import c.universe._
//    val Literal(Constant(v:Int)) = cond.tree
    //这样写编译时就会打印,而不是运行时
//    q"""${println(cond)}"""
    //这样才起作用
    q"""println($cond)"""
  }
现在在来分析第一个例子,首先通过解构,拿到表达式的每一条语句(都是一个tree),然后通过flatMap将一条语句变成两条,第一条是打印语句表达式,第二个就是原语句不变,所以在使用部分的代码变成如下形式:
      println("val a = 1")    
      val a = 1
      println("val b = a.+(2)")
      val b = a + 2
      println("a.+(b)")
      a + b
scala的宏我能介绍的差不多就这些了,还有一些特性,如宏注解、类型提供者等等,我也在研究中,并思考其用途。


分类   scala
字数   3443

博客标签    scala   宏编程  

评论