在上期討論中咱們介紹了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編程工具。git
我在介紹了Slick以後當即轉入Scala Macros是有一些特別目的的。研究FRM Slick乃至學習泛函編程的初衷就是但願能爲傳統的OOP編程人員提供更簡單易用的泛函庫應用幫助,使他們無須對函數式編程模式有太深入瞭解也能使用由函數式編程模式所開發的函數庫。實現這個目標的主要方式就是Macros了。但願經過Macros的產生代碼功能把函數庫的泛函特性和模式屏蔽起來,讓用戶能用他們習慣的方式來定義函數庫中的類型對象、調用庫中的方法函數。github
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的這種依賴關係。api
1 name := "learn-scalameta"
2
3 val commonSettings = Seq( 4 version := "1.0" , 5 scalaVersion := "2.11.8", 6 scalacOptions ++= Seq("-deprecation", "-feature"), 7 resolvers += Resolver.sonatypeRepo("snapshots"), 8 addCompilerPlugin( 9 "org.scalameta" % "paradise" % "3.0.0-M5" cross CrossVersion.full), 10 scalacOptions += "-Xplugin-require:macroparadise"
11
12 ) 13 val macrosSettings = Seq( 14 libraryDependencies += "org.scalameta" %% "scalameta" % "1.3.0", 15 libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
16 ) 17 lazy val root = (project in file(".")).aggregate(macros, demos) 18
19 lazy val macros = project.in(file("macros")). 20 settings(commonSettings : _*). 21 settings(macrosSettings : _*) 22
23 lazy val demos = project.in(file("demos")).settings(commonSettings : _*).dependsOn(macros)
下面咱們先用一個最簡單的例子來開始瞭解Scalameta Macros Annotations:安全
1 object MacroAnnotDemo extends App { 2
3 @Greetings object Greet { 4 def add(x: Int, y: Int) = println(x + y) 5 } 6
7 Greet.sayHello("John") 8 Greet.add(1,2) 9 }
這裏的註釋@Greetings表明被註釋對象Greet將會被擴展增長一個sayHello的函數。咱們看看這個註釋的實現方式:app
1 import scala.meta._ 2
3 class Greetings extends scala.annotation.StaticAnnotation { 4 inline def apply(defn: Any): Any = meta { 5 defn match { 6 case q"object $name {..$stats}" => { 7 q""" 8 object $name { 9 def sayHello(msg: String): Unit = println("Hello," + msg) 10 ..$stats 11 } 12 """ 13 } 14 case _ => abort("annottee must be object!") 15 } 16 } 17 }
首先,咱們看到這段源代碼表達方式直接了許多:只須要import scala.meta,沒有了blackbox、whitebox、universe這些imports。特別是避免了對blackbox.Context和whitebox.Context這些複雜運算域的人爲斷定。quasiquote的使用沒有什麼變化。直觀上Macros編程簡單了,實際上編寫的Macros程序能更安全穩定的運行。dom
咱們再重複演示方法註釋(method annotation)的實現方法:函數式編程
1 class Benchmark extends scala.annotation.StaticAnnotation { 2 inline def apply(defn: Any): Any = meta { 3 defn match { 4 case q"..$mod def $name[..$tparams](...$args): $rtpe = $body" =>
5 q""" 6 ..$mod def $name[..$tparams](...$args): $rtpe = { 7 val start = System.nanoTime() 8 val result = $body 9 val end = System.nanoTime() 10 println(${name.toString} + " elapsed time = " + (end - start) + "ns") 11 result 12 } 13 """ 14 case _ => abort("Fail to expand annotation Benchmark!") 15 } 16 } 17 }
仍是固定格式。只是quasiquote的調用組合變化。用下面方法調用測試:函數
1 @Benchmark 2 def calcPow(x: Double, y: Double) = { 3 val z = x + y 4 math.pow(z,z) 5 } 6
7 println(calcPow(4.2, 8.9))
在下面這個例子裏咱們在註釋對象中增長main方法(未extends App的對象):工具
1 import scala.meta.Ctor.Call 2 class main extends scala.annotation.StaticAnnotation { 3 inline def apply(defn: Any): Any = meta { 4 def abortIfObjectAlreadyExtendsApp(ctorcalls: scala.collection.immutable.Seq[Call], objectName: Term) = { 5 val extendsAppAlready = ctorcalls.map(_.structure).contains(ctor"App()".structure) 6 if (extendsAppAlready){ 7 abort(s"$objectName already extends App") 8 } 9 } 10 defn match { 11 case q"..$mods object $name extends $template" => template match { 12 case template"{ ..$stats1 } with ..$ctorcalls { $param => ..$stats2 }" =>
13 abortIfObjectAlreadyExtendsApp(ctorcalls, name) 14 val mainMethod = q"def main(args: Array[String]): Unit = { ..$stats2 }"
15 val newTemplate = template"{ ..$stats1 } with ..$ctorcalls { $param => $mainMethod }"
16
17 q"..$mods object $name extends $newTemplate"
18 } 19 case _ => abort("@main can be annotation of object only") 20 } 21 } 22 }
下面這個是case class的註釋示例:效果是添加一個從case class轉Map的類型轉換函數toMap:
1 @compileTimeOnly("@Mappable not expanded") 2 class Mappable extends StaticAnnotation { 3 inline def apply(defn: Any): Any = meta { 4 defn match { 5 case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
6 template match { 7 case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => { 8 val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{ 9 case param"..$mods $paramname: $atpeopt = $expropt" => paramname 10 }).map{case (q"$paramName", paramTree) => { 11 q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"
12 }} 13
14 val resultMap = q"Map(..$expr)"
15
16 val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""
17 val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"
18
19 q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
20 } 21 } 22 case _ => throw new Exception("@Mappable can be annotation of class only") 23 } 24 } 25 }
能夠用下面的數據進行測試:
1 @Mappable 2 case class Car(color: String, model: String, year: Int, owner: String){ 3 def turnOnRadio = { 4 "playing"
5 } 6 } 7
8 val newCarMap = Car("Silver", "Ford", 1998, "John Doe").toMap 9 println(newCarMap)
在下面這個例子裏示範瞭如何使用註釋參數:
1 import scala.util.Try 2 @compileTimeOnly("@RetryOnFailure not expanded") 3 class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation { 4 inline def apply(defn: Any): Any = meta { 5 defn match { 6 case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => { 7 val q"new $_(${arg})" = this
8 val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter")) 9
10 val newCode =
11 q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {
12 import scala.util.Try 13
14 for( a <- 1 to $repeats){ 15 val res = Try($expr) 16 if(res.isSuccess){ 17 return res.get
18 } 19 } 20
21 throw new Exception("Method fails after "+$repeats + " repeats") 22 } 23 """ 24 newCode 25 } 26 case _ => abort("@RetryOnFailure can be annotation of method only") 27 } 28 } 29 }
具體使用方法以下:
object utils { def methodThrowingException(random: Int): Unit = { if(random%2 == 0){ throw new Exception(s"throwing exception for ${random}") } } } import scala.util.Random @RetryOnFailure(20) def failMethod[String](): Unit = { val random = Random.nextInt(10) println("Retrying...") utils.methodThrowingException(random) }
順便也把上次的那個TalkingAnimal從新再寫一下:
1 class TalkingAnimal(voice: String) extends StaticAnnotation { 2 inline def apply(defn: Any): Any = meta { 3 defn match { 4 case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
5 template match { 6 case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => { 7 val q"new $_(${arg})" = this
8 val sound = arg.toString() 9 val animalType = tname.toString() 10 val newBody = body :+
11 q""" def sayHello: Unit =
12 println("Hello, I'm a " + $animalType +
13 " and my name is " + name + " " + $sound+ "...") 14 """ 15 val newTemplate =template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"
16 q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
17 } 18 } 19 case _ => abort("Error: expanding TalkingAnimal!") 20 } 21 } 22 }
對比舊款Def Macros能夠發現quasiquote的語法仍是有變化的,好比拆分class定義就須要先拆出template。Scalameta從新定義了新的quasiquote,另外註釋對象參數的運算方法也有所不一樣,這是由於Scalameta的AST新設計的表達結構。
測試運算以下:
1 trait Animal { 2 val name: String 3 } 4 @TalkingAnimal("wangwang") 5 case class Dog(val name: String) extends Animal 6
7 @TalkingAnimal("miaomiao") 8 case class Cat(val name: String) extends Animal 9
10 //@TalkingAnimal("") 11 //case class Carrot(val name: String) 12 //Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal
13 Dog("Goldy").sayHello 14 Cat("Kitty").sayHello
下面是本次討論中的完整示範源代碼:
註釋實現源代碼:
1 import scala.meta._ 2 class Greetings extends scala.annotation.StaticAnnotation { 3 inline def apply(defn: Any): Any = meta { 4 defn match { 5 case q"object $name {..$stats}" => { 6 q""" 7 object $name { 8 def sayHello(msg: String): Unit = println("Hello," + msg) 9 ..$stats 10 } 11 """ 12 } 13 case q"object $name extends $parent {..$stats}" => { 14 q""" 15 object $name extends $parent { 16 def sayHello(msg: String): Unit = println("Hello," + msg) 17 ..$stats 18 } 19 """ 20 } 21 case _ => abort("annottee must be object!") 22 } 23 } 24 } 25
26 class Benchmark extends scala.annotation.StaticAnnotation { 27 inline def apply(defn: Any): Any = meta { 28 defn match { 29 case q"..$mod def $name[..$tparams](...$args): $rtpe = $body" =>
30 q""" 31 ..$mod def $name[..$tparams](...$args): $rtpe = { 32 val start = System.nanoTime() 33 val result = $body 34 val end = System.nanoTime() 35 println(${name.toString} + " elapsed time = " + (end - start) + "ns") 36 result 37 } 38 """ 39 case _ => abort("Fail to expand annotation Benchmark!") 40 } 41 } 42 } 43
44 import scala.meta.Ctor.Call 45 class main extends scala.annotation.StaticAnnotation { 46 inline def apply(defn: Any): Any = meta { 47 def abortIfObjectAlreadyExtendsApp(ctorcalls: scala.collection.immutable.Seq[Call], objectName: Term) = { 48 val extendsAppAlready = ctorcalls.map(_.structure).contains(ctor"App()".structure) 49 if (extendsAppAlready){ 50 abort(s"$objectName already extends App") 51 } 52 } 53 defn match { 54 case q"..$mods object $name extends $template" => template match { 55 case template"{ ..$stats1 } with ..$ctorcalls { $param => ..$stats2 }" =>
56 abortIfObjectAlreadyExtendsApp(ctorcalls, name) 57 val mainMethod = q"def main(args: Array[String]): Unit = { ..$stats2 }"
58 val newTemplate = template"{ ..$stats1 } with ..$ctorcalls { $param => $mainMethod }"
59
60 q"..$mods object $name extends $newTemplate"
61 } 62 case _ => abort("@main can be annotation of object only") 63 } 64 } 65 } 66 import scala.annotation.{StaticAnnotation, compileTimeOnly} 67 @compileTimeOnly("@Mappable not expanded") 68 class Mappable extends StaticAnnotation { 69 inline def apply(defn: Any): Any = meta { 70 defn match { 71 case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
72 template match { 73 case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => { 74 val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{ 75 case param"..$mods $paramname: $atpeopt = $expropt" => paramname 76 }).map{case (q"$paramName", paramTree) => { 77 q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"
78 }} 79
80 val resultMap = q"Map(..$expr)"
81
82 val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""
83 val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"
84
85 q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
86 } 87 } 88 case _ => throw new Exception("@Mappable can be annotation of class only") 89 } 90 } 91 } 92 import scala.util.Try 93 @compileTimeOnly("@RetryOnFailure not expanded") 94 class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation { 95 inline def apply(defn: Any): Any = meta { 96 defn match { 97 case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => { 98 val q"new $_(${arg})" = this
99 val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter")) 100
101 val newCode =
102 q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {
103 import scala.util.Try 104
105 for( a <- 1 to $repeats){ 106 val res = Try($expr) 107 if(res.isSuccess){ 108 return res.get
109 } 110 } 111
112 throw new Exception("Method fails after "+$repeats + " repeats") 113 } 114 """ 115 newCode 116 } 117 case _ => abort("@RetryOnFailure can be annotation of method only") 118 } 119 } 120 } 121
122 class TalkingAnimal(voice: String) extends StaticAnnotation { 123 inline def apply(defn: Any): Any = meta { 124 defn match { 125 case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
126 template match { 127 case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => { 128 val q"new $_(${arg})" = this
129 val sound = arg.toString() 130 val animalType = tname.toString() 131 val newBody = body :+
132 q""" def sayHello: Unit =
133 println("Hello, I'm a " + $animalType +
134 " and my name is " + name + " " + $sound+ "...") 135 """ 136 val newTemplate =template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"
137 q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
138 } 139 } 140 case _ => abort("Error: expanding TalkingAnimal!") 141 } 142 } 143 }
試運行代碼:
1 object MacroAnnotDemo extends App { 2 @Greetings object Greet { 3 def add(x: Int, y: Int) = println(x + y) 4 } 5 @Greetings object Hi extends AnyRef {} 6 7 Greet.sayHello("John") 8 Greet.add(1,2) 9 Hi.sayHello("Susana Wang") 10 11 @Benchmark 12 def calcPow(x: Double, y: Double) = { 13 val z = x + y 14 math.pow(z,z) 15 } 16 17 println(calcPow(4.2, 8.9)) 18 19 @Mappable 20 case class Car(color: String, model: String, year: Int, owner: String){ 21 def turnOnRadio = { 22 "playing" 23 } 24 } 25 26 val newCarMap = Car("Silver", "Ford", 1998, "John Doe").toMap 27 println(newCarMap) 28 29 object utils { 30 def methodThrowingException(random: Int): Unit = { 31 if(random%2 == 0){ 32 throw new Exception(s"throwing exception for ${random}") 33 } 34 } 35 } 36 import scala.util.Random 37 @RetryOnFailure(20) def failMethod[String](): Unit = { 38 val random = Random.nextInt(10) 39 println("Retrying...") 40 utils.methodThrowingException(random) 41 } 42 43 trait Animal { 44 val name: String 45 } 46 @TalkingAnimal("wangwang") 47 case class Dog(val name: String) extends Animal 48 49 @TalkingAnimal("miaomiao") 50 case class Cat(val name: String) extends Animal 51 52 //@TalkingAnimal("") 53 //case class Carrot(val name: String) 54 //Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal 55 Dog("Goldy").sayHello 56 Cat("Kitty").sayHello 57 58 }