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的使用難度能降低一個臺階。