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 [email protected](TermName(name)) => (tag, name) :: lis case [email protected](se, TermName(t)) => reverPath(se, (tag, t) :: lis) case [email protected](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的使用难度能下降一个台阶。