在上期讨论中我们介绍了Scala Macros,它可以说是工具库编程人员不可或缺的编程手段,可以实现编译器在编译源代码时对源代码进行的修改、扩展和替换,如此可以对用户屏蔽工具库复杂的内部细节,使他们可以用简单的声明方式,通过编译器自动产生铺垫代码来实现工具库中各种复杂的类型、对象及方法函数的构建。虽然Def Macros可能具备超强的编程功能,但同时使用者也普遍认为它一直存有着一些严重的诟病:包括用法复杂、容易犯错、运算行为难以预测以及没用完善的集成开发环境工具(IDE)支持等。这些恶评主要是因为Def Macros和编译器scalac捆绑的太紧,使用者必须对编译器的内部运作原理和操作函数有比较深刻的了解。加之Def Macros向用户提供的api比较复杂且调用繁琐,其中比较致命的问题就是与scalac的紧密捆绑了:因为Def Macros还只是一项实验性功能,没有scala语言规范文件背书,肯定会面临升级换代。而且scala本身也面临着向2.12版本升级的情况,其中dotty就肯定是scalac的替代编译器。Scalameta是根据scala语言规范SIP-28-29-Inline-Macros由零重新设计的Macros编程工具库。主要目的就是为了解决Def Macros所存在的问题,而且Jetbrains的IntelliJ IDEA 2016.3 EAP对Scalameta已经有了比较好的支持,能为使用者带来更简单、安全的Macros编程工具。

我在介绍了Slick之后立即转入Scala Macros是有一些特别目的的。研究FRM Slick乃至学习泛函编程的初衷就是希望能为传统的OOP编程人员提供更简单易用的泛函库应用帮助,使他们无须对函数式编程模式有太深刻了解也能使用由函数式编程模式所开发的函数库。实现这个目标的主要方式就是Macros了。希望通过Macros的产生代码功能把函数库的泛函特性和模式屏蔽起来,让用户能用他们习惯的方式来定义函数库中的类型对象、调用库中的方法函数。

Macros功能实现方式(即编译时的源代码扩展compile time expansion)由两个主要部分组成:一是在调用时扩展(on call expansion),二是申明时扩展即注释(annotation)。这两种方式我们在上一篇讨论里都一一做了示范。通过测试发现,Scalameta v1.x只支持注释方式。这事动摇了我继续探讨的意愿:试想如果没了”Implicit Macros“,“Extractor Macros“这些模式,会损失多少理想有趣的编码方式。通过与Scalameta作者沟通后得知他们将会在Scalameta v2.x中开始支持全部两种模式,因此决定先介绍一下Scalameta v1.x,主要目的是让大家先熟悉了解Scalameta新的api和使用模式。我们可以把上次Def Macros的Macros Annotations示范例子在Scalameta里重新示范一遍来达到这样的目的。

虽然Scalameta是从头设计的,但是它还是保留了许多Def Macros的思想,特别是沿用了大部分scala-reflect的quasiquote模式。与Def Macros运算原理相同,Scalameta的Macros扩展也是基于AST(abstract syntax tree)由编译器运算产生的,因此Macros申明必须先完成编译,所以我们还是沿用了上一篇讨论中的build.sbt,保留项目结构,及demos对macros的这种依赖关系。

 name := "learn-scalameta"

 val commonSettings = Seq(<br/>
   version := "1.0" ,<br/>
   scalaVersion := "2.11.8",<br/>
   scalacOptions ++= Seq("-deprecation", "-feature"),<br/>
   resolvers += Resolver.sonatypeRepo("snapshots"),<br/>
   addCompilerPlugin(<br/>
     "org.scalameta" % "paradise" % "3.0.0-M5" cross CrossVersion.full),<br/>
   scalacOptions += "-Xplugin-require:macroparadise"

 )<br/>
 val macrosSettings = Seq(<br/>
   libraryDependencies += "org.scalameta" %% "scalameta" % "1.3.0",<br/>
   libraryDependencies +=  "org.scalatest" %% "scalatest" % "3.0.1" % "test"<br/>
 )<br/>
 lazy val root = (project in file(".")).aggregate(macros, demos)

 lazy val macros  = project.in(file("macros")).<br/>
   settings(commonSettings : _*).<br/>
   settings(macrosSettings : _*)

 lazy val demos  = project.in(file("demos")).settings(commonSettings : _*).dependsOn(macros)

下面我们先用一个最简单的例子来开始了解Scalameta Macros Annotations:

 object MacroAnnotDemo extends App {

   @Greetings object Greet {<br/>
     def add(x: Int, y: Int) = println(x + y)<br/>
   }

   Greet.sayHello("John")<br/>
   Greet.add(,)<br/>
 }

这里的注释@Greetings代表被注释对象Greet将会被扩展增加一个sayHello的函数。我们看看这个注释的实现方式:

 import scala.meta._

 class Greetings extends scala.annotation.StaticAnnotation {<br/>
     inline def apply(defn: Any): Any = meta {<br/>
       defn match {<br/>
         case q"object $name {..$stats}" => {<br/>
           q"""<br/>
               object $name {<br/>
                 def sayHello(msg: String): Unit = println("Hello," + msg)<br/>
                 ..$stats<br/>
               }<br/>
             """<br/>
         }<br/>
         case _ => abort("annottee must be object!")<br/>
       }<br/>
     }<br/>
 }

首先,我们看到这段源代码表达方式直接了许多:只需要import scala.meta,没有了blackbox、whitebox、universe这些imports。特别是避免了对blackbox.Context和whitebox.Context这些复杂运算域的人为判定。quasiquote的使用没有什么变化。直观上Macros编程简单了,实际上编写的Macros程序能更安全稳定的运行。

我们再重复演示方法注释(method annotation)的实现方法:

 class Benchmark extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mod def $name[..$tparams](...$args): $rtpe = $body" =><br/>
         q"""<br/>
             ..$mod def $name[..$tparams](...$args): $rtpe = {<br/>
             val start = System.nanoTime()<br/>
             val result = $body<br/>
             val end = System.nanoTime()<br/>
             println(${name.toString} + " elapsed time = " + (end - start) + "ns")<br/>
             result<br/>
            }<br/>
           """<br/>
       case _ => abort("Fail to expand annotation Benchmark!")<br/>
     }<br/>
   }<br/>
 }

还是固定格式。只是quasiquote的调用组合变化。用下面方法调用测试:

   @Benchmark<br/>
   def calcPow(x: Double, y: Double) = {<br/>
     val z = x + y<br/>
     math.pow(z,z)<br/>
   }

   println(calcPow(4.2, 8.9))

在下面这个例子里我们在注释对象中增加main方法(未extends App的对象):

 import scala.meta.Ctor.Call<br/>
 class main extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     def abortIfObjectAlreadyExtendsApp(ctorcalls: scala.collection.immutable.Seq[Call], objectName: Term) = {<br/>
       val extendsAppAlready = ctorcalls.map(_.structure).contains(ctor"App()".structure)<br/>
       if (extendsAppAlready){<br/>
         abort(s"$objectName already extends App")<br/>
       }<br/>
     }<br/>
     defn match {<br/>
       case q"..$mods object $name extends $template" => template match {<br/>
         case template"{ ..$stats1 } with ..$ctorcalls { $param => ..$stats2 }" =><br/>
           abortIfObjectAlreadyExtendsApp(ctorcalls, name)<br/>
           val mainMethod = q"def main(args: Array[String]): Unit = { ..$stats2 }"<br/>
           val newTemplate = template"{ ..$stats1 } with ..$ctorcalls { $param => $mainMethod }"

           q"..$mods object $name extends $newTemplate"<br/>
       }<br/>
       case _ => abort("@main can be annotation of object only")<br/>
     }<br/>
   }<br/>
 }

下面这个是case class的注释示例:效果是添加一个从case class转Map的类型转换函数toMap:

 @compileTimeOnly("@Mappable not expanded")<br/>
 class Mappable extends StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =><br/>
         template match {<br/>
           case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {<br/>
             val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{<br/>
               case param"..$mods $paramname: $atpeopt = $expropt" => paramname<br/>
             }).map{case (q"$paramName", paramTree) => {<br/>
               q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"<br/>
             }}

             val resultMap = q"Map(..$expr)"

             val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""<br/>
             val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"

             q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"<br/>
           }<br/>
         }<br/>
       case _ => throw new Exception("@Mappable can be annotation of class only")<br/>
     }<br/>
   }<br/>
 }

可以用下面的数据进行测试:

   @Mappable<br/>
   case class Car(color: String, model: String, year: Int, owner: String){<br/>
     def turnOnRadio = {<br/>
       "playing"<br/>
     }<br/>
   }

   val newCarMap = Car("Silver", "Ford", , "John Doe").toMap<br/>
   println(newCarMap)

在下面这个例子里示范了如何使用注释参数:

 import scala.util.Try<br/>
 @compileTimeOnly("@RetryOnFailure not expanded")<br/>
 class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => {<br/>
         val q"new $_(${arg})" = this<br/>
         val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter"))

         val newCode =<br/>
           q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {<br/>
                 import scala.util.Try

                 for( a <-  to $repeats){<br/>
                   val res = Try($expr)<br/>
                   if(res.isSuccess){<br/>
                     return res.get<br/>
                   }<br/>
                 }

                 throw new Exception("Method fails after "+$repeats + " repeats")<br/>
               }<br/>
             """<br/>
         newCode<br/>
       }<br/>
       case _ => abort("@RetryOnFailure can be annotation of method only")<br/>
     }<br/>
   }<br/>
 }

具体使用方法如下:

 object utils {<br/>
    def methodThrowingException(random: Int): Unit = {<br/>
      if(random% == ){<br/>
        throw new Exception(s"throwing exception for ${random}")<br/>
      }<br/>
    }<br/>
  }<br/>
  import scala.util.Random<br/>
  @RetryOnFailure() def failMethod[String](): Unit = {<br/>
    val random = Random.nextInt()<br/>
    println("Retrying...")<br/>
    utils.methodThrowingException(random)<br/>
  }

顺便也把上次的那个TalkingAnimal重新再写一下:

 class TalkingAnimal(voice: String) extends StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =><br/>
         template match {<br/>
           case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {<br/>
             val q"new $_(${arg})" = this<br/>
             val sound = arg.toString()<br/>
             val animalType = tname.toString()<br/>
             val newBody = body :+<br/>
               q""" def sayHello: Unit =<br/>
                      println("Hello, I'm a " + $animalType +<br/>
                     " and my name is " + name + " " + $sound+ "...")<br/>
               """<br/>
             val newTemplate =template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"<br/>
             q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"<br/>
           }<br/>
         }<br/>
       case _ => abort("Error: expanding TalkingAnimal!")<br/>
     }<br/>
   }<br/>
 }

对比旧款Def Macros可以发现quasiquote的语法还是有变化的,比如拆分class定义就需要先拆出template。Scalameta重新定义了新的quasiquote,另外注释对象参数的运算方法也有所不同,这是因为Scalameta的AST新设计的表达结构。

测试运算如下:

   trait Animal {<br/>
     val name: String<br/>
   }<br/>
   @TalkingAnimal("wangwang")<br/>
   case class Dog(val name: String) extends Animal

   @TalkingAnimal("miaomiao")<br/>
   case class Cat(val name: String) extends Animal

   //@TalkingAnimal("")<br/>
   //case class Carrot(val name: String)<br/>
   //Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal<br/>
   Dog("Goldy").sayHello<br/>
   Cat("Kitty").sayHello

下面是本次讨论中的完整示范源代码:

注释实现源代码:

 import scala.meta._<br/>
 class Greetings extends scala.annotation.StaticAnnotation {<br/>
     inline def apply(defn: Any): Any = meta {<br/>
       defn match {<br/>
         case q"object $name {..$stats}" => {<br/>
           q"""<br/>
               object $name {<br/>
                 def sayHello(msg: String): Unit = println("Hello," + msg)<br/>
                 ..$stats<br/>
               }<br/>
             """<br/>
         }<br/>
         case q"object $name extends $parent {..$stats}" => {<br/>
             q"""<br/>
               object $name extends $parent {<br/>
                 def sayHello(msg: String): Unit = println("Hello," + msg)<br/>
                 ..$stats<br/>
               }<br/>
             """<br/>
         }<br/>
         case _ => abort("annottee must be object!")<br/>
       }<br/>
     }<br/>
 }

 class Benchmark extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mod def $name[..$tparams](...$args): $rtpe = $body" =><br/>
         q"""<br/>
             ..$mod def $name[..$tparams](...$args): $rtpe = {<br/>
             val start = System.nanoTime()<br/>
             val result = $body<br/>
             val end = System.nanoTime()<br/>
             println(${name.toString} + " elapsed time = " + (end - start) + "ns")<br/>
             result<br/>
            }<br/>
           """<br/>
       case _ => abort("Fail to expand annotation Benchmark!")<br/>
     }<br/>
   }<br/>
 }

 import scala.meta.Ctor.Call<br/>
 class main extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     def abortIfObjectAlreadyExtendsApp(ctorcalls: scala.collection.immutable.Seq[Call], objectName: Term) = {<br/>
       val extendsAppAlready = ctorcalls.map(_.structure).contains(ctor"App()".structure)<br/>
       if (extendsAppAlready){<br/>
         abort(s"$objectName already extends App")<br/>
       }<br/>
     }<br/>
     defn match {<br/>
       case q"..$mods object $name extends $template" => template match {<br/>
         case template"{ ..$stats1 } with ..$ctorcalls { $param => ..$stats2 }" =><br/>
           abortIfObjectAlreadyExtendsApp(ctorcalls, name)<br/>
           val mainMethod = q"def main(args: Array[String]): Unit = { ..$stats2 }"<br/>
           val newTemplate = template"{ ..$stats1 } with ..$ctorcalls { $param => $mainMethod }"

           q"..$mods object $name extends $newTemplate"<br/>
       }<br/>
       case _ => abort("@main can be annotation of object only")<br/>
     }<br/>
   }<br/>
 }<br/>
 import scala.annotation.{StaticAnnotation, compileTimeOnly}<br/>
 @compileTimeOnly("@Mappable not expanded")<br/>
 class Mappable extends StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =><br/>
         template match {<br/>
           case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {<br/>
             val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{<br/>
               case param"..$mods $paramname: $atpeopt = $expropt" => paramname<br/>
             }).map{case (q"$paramName", paramTree) => {<br/>
               q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"<br/>
             }}

             val resultMap = q"Map(..$expr)"

             val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""<br/>
             val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"

             q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"<br/>
           }<br/>
         }<br/>
       case _ => throw new Exception("@Mappable can be annotation of class only")<br/>
     }<br/>
   }<br/>
 }<br/>
 import scala.util.Try<br/>
 @compileTimeOnly("@RetryOnFailure not expanded")<br/>
 class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => {<br/>
         val q"new $_(${arg})" = this<br/>
         val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter"))

         val newCode =<br/>
           q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {<br/>
                 import scala.util.Try

                 for( a <-  to $repeats){<br/>
                   val res = Try($expr)<br/>
                   if(res.isSuccess){<br/>
                     return res.get<br/>
                   }<br/>
                 }

                 throw new Exception("Method fails after "+$repeats + " repeats")<br/>
               }<br/>
             """<br/>
         newCode<br/>
       }<br/>
       case _ => abort("@RetryOnFailure can be annotation of method only")<br/>
     }<br/>
   }<br/>
 }

 class TalkingAnimal(voice: String) extends StaticAnnotation {<br/>
   inline def apply(defn: Any): Any = meta {<br/>
     defn match {<br/>
       case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =><br/>
         template match {<br/>
           case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {<br/>
             val q"new $_(${arg})" = this<br/>
             val sound = arg.toString()<br/>
             val animalType = tname.toString()<br/>
             val newBody = body :+<br/>
               q""" def sayHello: Unit =<br/>
                      println("Hello, I'm a " + $animalType +<br/>
                     " and my name is " + name + " " + $sound+ "...")<br/>
               """<br/>
             val newTemplate =template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"<br/>
             q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"<br/>
           }<br/>
         }<br/>
       case _ => abort("Error: expanding TalkingAnimal!")<br/>
     }<br/>
   }<br/>
 }

试运行代码:

 object MacroAnnotDemo extends App {<br/>
   @Greetings object Greet {<br/>
     def add(x: Int, y: Int) = println(x + y)<br/>
   }<br/>
   @Greetings object Hi extends AnyRef {}

   Greet.sayHello("John")<br/>
   Greet.add(,)<br/>
   Hi.sayHello("Susana Wang")

   @Benchmark<br/>
   def calcPow(x: Double, y: Double) = {<br/>
     val z = x + y<br/>
     math.pow(z,z)<br/>
   }

   println(calcPow(4.2, 8.9))

   @Mappable<br/>
   case class Car(color: String, model: String, year: Int, owner: String){<br/>
     def turnOnRadio = {<br/>
       "playing"<br/>
     }<br/>
   }

   val newCarMap = Car("Silver", "Ford", , "John Doe").toMap<br/>
   println(newCarMap)

   object utils {<br/>
     def methodThrowingException(random: Int): Unit = {<br/>
       if(random% == ){<br/>
         throw new Exception(s"throwing exception for ${random}")<br/>
       }<br/>
     }<br/>
   }<br/>
   import scala.util.Random<br/>
   @RetryOnFailure() def failMethod[String](): Unit = {<br/>
     val random = Random.nextInt()<br/>
     println("Retrying...")<br/>
     utils.methodThrowingException(random)<br/>
   }

   trait Animal {<br/>
     val name: String<br/>
   }<br/>
   @TalkingAnimal("wangwang")<br/>
   case class Dog(val name: String) extends Animal

   @TalkingAnimal("miaomiao")<br/>
   case class Cat(val name: String) extends Animal

   //@TalkingAnimal("")<br/>
   //case class Carrot(val name: String)<br/>
   //Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal<br/>
   Dog("Goldy").sayHello<br/>
   Cat("Kitty").sayHello

 }