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

时间: 2024-10-11 13:42:04

scala macro-使case copy易读的相关文章

pretty-errors:美化python异常输出以使其清晰易读

1. 安装pretty-errors python -m pip install pretty_errors 2.如果你想让你的每一个程序都能这样在报错时也保持美貌,那么运行下面这这行命令,就不用每次都 import pretty_errors .这是使用pretty_errors的推荐方法:除了更简单和通用之外,使用它意味着SyntaxError异常也会得到prettly格式化(如果手动导入pretty_errors,则这不起作用). python -m pretty_errors 如果您还没

对JdbcTemplate进行简易封装以使其更加易用

在上一篇博文<基于SpringJDBC的类mybatis形式SQL语句管理的思考与实现>中,我们实现了采用XML文件对SQL进行管理.在本篇博文中,我们将对SpringJDBC提供的JdbcTemplate进行简易封装,使其更加的易用,更加贴近上篇博文中对于SQL管理的设计. 我们希望在使用将要封装的这个工具进行数据库操作时有以下几个优势: 不处理数据获取异常 不关心日志记录 即可以使用我们XML文件中的SQL语句,也可以使用在业务方法中定义的SQL语句.(因为我们设计的XML文件并不能够非常

Selenium 2自动化测试实战36(更易读的测试报告)

一.更易读的测试报告 1.知识点:python的注释. 1.一种叫comment,为普通的注释2.另一种叫doc string,用于函数,类和方法的描述.在类或方法的下方,通过三引号(""" """或''' ''')来添加doc string类型的注释,这类注释在平时调用的时候不显示,可以通过help()方法来查看类或方法的这种注释. HTMLTestRunner可以读取doc string类型的注释,所以,我们只需要给测试类或方法添加这种类型的

web设计_7_页面缺失图片或CSS的情况下仍然易读

1. 在任何可能使用背景图片的地方应设置同样的颜色的背景色. 防止图片不能加载的情况下,页面内容同样保持较好可读性. 例如文字为白色,背景图为深色,如果不设置背景色,当背景图未成功加载, 而浏览器多数默认背景为白色,那么这是文字内容无法可读. 2.当禁用CSS样式后,web仍然能够呈现很好地内容页面. 需要能够较好的保证页面核心内容与样式很好的分离.做到清晰易读的结构代码. 利用firebug等工具可进行实施观察,修改和调试. 3.利用W3C的HTML代码验证器 http://validator

显示列表中的xenserver的总内存,可用内存,可用内存比【易读版】

9==显示列表中的xenserver的总内存,可用内存,可用内存比[易读版]-----------------------for I in $(cat <<eof | cat PTtile192.168.1.xxx 192.168.1.xxx eof );do {[ "$I" = "PTtile" ] && echo -e "NameLabel:\t\tIpAddress:\t\tHostName:\t\tMemTotal:\t

Scala class和case class的区别

在Scala中存在case class,它其实就是一个普通的class.但是它又和普通的class略有区别,如下: 1.初始化的时候可以不用new,当然你也可以加上,普通类一定需要加new: scala> case class Iteblog(name:String) defined class Iteblog scala> val iteblog = Iteblog("iteblog_hadoop") iteblog: Iteblog = Iteblog(iteblog_

scala学习手记 - case表达式里的模式变量和常量

再来看一下之前的一段代码: def process(input: Any) { input match { case (a: Int, b: Int) => println("Processing (int, int)... ") case (a: Double, b: Double) => println("Processing (double, double)... ") case msg: Int if (msg > 1000000) =&g

大数据学习:Scala面向对象和Spark一些代码读和问

画外音: Spark对面向对象的支持是非常完美的 主题: 1.简单的类: 2.重写getter.setter方法: 3.利用其它方法来控制外部对值的控制: 4. private[this]: 5.构造器以及构造器相关: 直接代码见真章: ==========最简单的类============ scala> class HiScala{ | private var name = "Spark" | def sayName(){println(name)} | def getName

Scala匹配模式---Case 类匹配

让我们来尝试一次深度匹配,在我们的模式匹配中检查对象的内容. //code-examples/Rounding/match-deep-script.scala case class Person(name: String, age:Int) val alice = new Person("Alice",25) val bob = new Person("Bob",32) val charlie = newPerson("Charlie", 32)