scala macro-使case copy易读

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

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

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`,迫于时间压力和难度太大,只好用github

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。ide

//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,这里就不详细写了,有兴趣的,点击这里!post

好了,如今资料看完了,项目也有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来搞定。ui

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和需求进行构造目标代码了。
刚开始打算构造this

//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

但发现,太难写,上一行的代码被下一行代码使用,而且须要建立临时变量,因而改成递归的写法,去除临时变量。idea

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的使用难度能降低一个台阶。

相关文章
相关标签/搜索