scala macro-使case copy易读

ps:好久没写blog了,1是没时间写,2也是没啥干货。最近终于积累了些东西,可以拿出来晒晒。哈哈。

先说需求吧,boss让我将case class copy 的代码简化,使之易读。

case class A(a:String,b:Int)
case class B(a:A,b:Int,c:String)

val b = B(A("a",2),3,"c")
b.copy(a.copy(b=2))
//上面是简单的例子,如果case class 多重嵌套时,就会产生类似
//a.copy(b.copy(c.copy(d.copy.... 超长的代码。

当时为了解决它,搜了好多,`scala dynamic`等,还是没找到理想的解决方案,至于`macro`,迫于时间压力和难度太大,只好用

case class A(a:String,b:Int)
case class B(a:A,b:Int,c:String){
    def aa=a.a
    def aa_=(value:String)=this.copy(a.copy(a=value))
}

这种比较挫的解决方案。做完之后,还是一直比较抑郁,这么挫的方案无法接受啊,尤其是知道macro是能以比较优雅优雅的方式解决这个问题。
于是,折腾之路便开始了。这里先列出一些个人认为十分有用的资料:
macro 官方文档
Exploring Scala Macros: Map to Case Class Conversion
Scala Macros: Let Our Powers Combine!
Learning Scala Macros
Adding Reflection to Scala Macros
git: underscoreio/essential-macros
stackoverflow: Where can I learn about constructing AST‘s for Scala macros?

每接触一个新的东西,最最麻烦的就是起步,scala macro也不例外,光创建一个idea项目外链另一个项目就够费劲,不支持同时在同一个目录下编辑多个项目,现在idea出了14,解决了这一问题。这里给列下两个项目的build.sbt。

//core
organization := "timzaak"

name := "core"

version := "0.1-SNAPSHOT"

scalaVersion := "2.11.4"lazy val macrolib = RootProject(file("../macrolib"))

lazy val core = project.in(file(".")).aggregate(macrolib).dependsOn(macrolib)
//macro 
liborganization := "timzaak"

name := "macrolib"

version := "1.0.1"scalaVersion := "2.11.4"libraryDependencies ++= Seq(   
 "org.scala-lang" % "scala-reflect" % scalaVersion.value,    
 "org.scala-lang" % "scala-compiler" % scalaVersion.value)

项目搭建后,就是hello world,这里就不详细写了,有兴趣的,点击这里!

好了,现在资料看完了,项目也有hello world了,我们开始解决问题吧。刚开始,我把dsl 设定为

case class A(a:String,b:Int)case class B(a:A,b:Int,c:String)

val b = B(A("a",2),3,"c")
copy(b.a.a="new string")//返回  B(A("new String",2),3,"c")

却发现,报错。始知macro没有我想的那么强大,不能直接更改语义,而是应该用来批量生成代码,减少人工重复代码。也或许是翻译成的原因吧。
那么,我们一步一步来。先解决如何生成a.copy(b.copy(...的问题。
要想解决他,就要知道AST张成什么样。我们用idea提供的worksheet来搞定。

import reflect.runtime.universe._case class C(c:String)case class A(a:Int,b:String,c:C)

val a = A(1,"",C(""))
showRaw(reify{a.copy(a=2)}.tree)//Apply(Select(Select(Ident(TermName("A$A....

然而,它仅能提供给我们一个参考,还是会有一些问题的。Learning Scala  Macros提供了一个解决方案。大家可以用用。
拿到ast,剩下的就是根据AST和需求进行构造目标代码了。
刚开始打算构造

//case class A(a:String,b:Int)
//case class B(a:A,b:Int,c:String)//val b = B(A("a",2),3,"c")//copy(b.a.a="new string")
//--要构造的代码val $temp = b.a.copy(a="new String")
val result = b.copy(a=$temp)
result

但发现,太难写,上一行的代码被下一行代码使用,并且需要创建临时变量,于是改为递归的写法,去除临时变量。

b.copy(a.copy(a="new String"))

这时,整个macro是:

object CaseCopy {
  def copy(a: Any, b:Any )  = macro imp
  def imp(c: Context)(a: c.Expr[Any], b: c.Expr[Any]) = {    import c.universe._

    def reverPath(v: c.Tree, lis: List[(c.Tree, String)]): List[(c.Tree, String)] = {
      v match {        case tag@Ident(TermName(name)) =>
          (tag, name) :: lis        case tag@Select(se, TermName(t)) =>
          reverPath(se, (tag, t) :: lis)        case thi@This(TypeName(name))=>
           (thi, name) :: lis        case Apply(a,_)=>
          reverPath(a,lis)        case Block(List(b),_)=>
          reverPath(b,lis)        case _ =>
          c.abort(v.pos, "only support case copy ")
      }
    }

    val (path, parm) = reverPath(a.tree, Nil).tail.unzip

   (path.init zip parm.tail).reverse.foldLeft(q"$b": Tree) {      case (re, (p, m)) =>
        q"$p.copy(${TermName(m)}=$re)"
    }
  }
}

运行一下,测试代码:

case class B(i: Int)case class ABC(a: Int, b: B)object CaseC extends App {
  import tim.casecopy.CaseCopy.copy
  val abc = ABC(1, B(2))
  println(copy(abc.a, 123))
}

输出的竟是()。细细查阅一边代码后,才发现没有设定返回值,立马加上。

...
...
  def copy[T](a: Any, b:Any ):T  = macro imp[T]
  def imp[T](c: Context)(a: c.Expr[Any], b: c.Expr[Any]):c.Expr[T] = {
 ... 
 ...

val re=(path.init zip parm.tail).reverse.foldLeft(q"$b": Tree) {case (re, (p, m)) =>        q"$p.copy(${TermName(m)}=$re)"    }
   c.Expr[T](re)
...
//测试
println(copy[ABC](abc.a, 123))

剩下的还有什么要解决呢?
println(copy[ABC](abc.a,"string"))也能通过编译的。类型并不安全。
我们在代码上,添加上这一判定即可。

if(!(b.actualType<:<a.actualType)){
      c.abort(b.tree.pos,s"b:${b.actualType} must be subtype of a:${a.actualType}")
    }

虽然仅仅40行的代码,但准备的时间超过40小时。这令我无比怀念js的动态生成代码的能力!
scala macro虽然在11.x依旧被标示为experimental,但官方承诺在不久的将变成正式库,希望到时候,macro的使用难度能下降一个台阶。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。