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

相關文章
相關標籤/搜索