在上期讨论中我们介绍了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(
version := "1.0" ,
scalaVersion := "2.11.8",
scalacOptions ++= Seq("-deprecation", "-feature"),
resolvers += Resolver.sonatypeRepo("snapshots"),
addCompilerPlugin(
"org.scalameta" % "paradise" % "3.0.0-M5" cross CrossVersion.full),
scalacOptions += "-Xplugin-require:macroparadise" )
val macrosSettings = Seq(
libraryDependencies += "org.scalameta" %% "scalameta" % "1.3.0",
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
)
lazy val root = (project in file(".")).aggregate(macros, demos) lazy val macros = project.in(file("macros")).
settings(commonSettings : _*).
settings(macrosSettings : _*) lazy val demos = project.in(file("demos")).settings(commonSettings : _*).dependsOn(macros)

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

 object MacroAnnotDemo extends App {

   @Greetings object Greet {
def add(x: Int, y: Int) = println(x + y)
} Greet.sayHello("John")
Greet.add(,)
}

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

 import scala.meta._

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

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

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

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

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

   @Benchmark
def calcPow(x: Double, y: Double) = {
val z = x + y
math.pow(z,z)
} println(calcPow(4.2, 8.9))

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

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

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

 @compileTimeOnly("@Mappable not expanded")
class Mappable extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
template match {
case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {
val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{
case param"..$mods $paramname: $atpeopt = $expropt" => paramname
}).map{case (q"$paramName", paramTree) => {
q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"
}} val resultMap = q"Map(..$expr)" val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""
val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }" q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
}
}
case _ => throw new Exception("@Mappable can be annotation of class only")
}
}
}

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

   @Mappable
case class Car(color: String, model: String, year: Int, owner: String){
def turnOnRadio = {
"playing"
}
} val newCarMap = Car("Silver", "Ford", , "John Doe").toMap
println(newCarMap)

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

 import scala.util.Try
@compileTimeOnly("@RetryOnFailure not expanded")
class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => {
val q"new $_(${arg})" = this
val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter")) val newCode =
q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {
import scala.util.Try for( a <- to $repeats){
val res = Try($expr)
if(res.isSuccess){
return res.get
}
} throw new Exception("Method fails after "+$repeats + " repeats")
}
"""
newCode
}
case _ => abort("@RetryOnFailure can be annotation of method only")
}
}
}

具体使用方法如下:

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

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

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

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

测试运算如下:

   trait Animal {
val name: String
}
@TalkingAnimal("wangwang")
case class Dog(val name: String) extends Animal @TalkingAnimal("miaomiao")
case class Cat(val name: String) extends Animal //@TalkingAnimal("")
//case class Carrot(val name: String)
//Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal
Dog("Goldy").sayHello
Cat("Kitty").sayHello

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

注释实现源代码:

 import scala.meta._
class Greetings extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"object $name {..$stats}" => {
q"""
object $name {
def sayHello(msg: String): Unit = println("Hello," + msg)
..$stats
}
"""
}
case q"object $name extends $parent {..$stats}" => {
q"""
object $name extends $parent {
def sayHello(msg: String): Unit = println("Hello," + msg)
..$stats
}
"""
}
case _ => abort("annottee must be object!")
}
}
} class Benchmark extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mod def $name[..$tparams](...$args): $rtpe = $body" =>
q"""
..$mod def $name[..$tparams](...$args): $rtpe = {
val start = System.nanoTime()
val result = $body
val end = System.nanoTime()
println(${name.toString} + " elapsed time = " + (end - start) + "ns")
result
}
"""
case _ => abort("Fail to expand annotation Benchmark!")
}
}
} import scala.meta.Ctor.Call
class main extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
def abortIfObjectAlreadyExtendsApp(ctorcalls: scala.collection.immutable.Seq[Call], objectName: Term) = {
val extendsAppAlready = ctorcalls.map(_.structure).contains(ctor"App()".structure)
if (extendsAppAlready){
abort(s"$objectName already extends App")
}
}
defn match {
case q"..$mods object $name extends $template" => template match {
case template"{ ..$stats1 } with ..$ctorcalls { $param => ..$stats2 }" =>
abortIfObjectAlreadyExtendsApp(ctorcalls, name)
val mainMethod = q"def main(args: Array[String]): Unit = { ..$stats2 }"
val newTemplate = template"{ ..$stats1 } with ..$ctorcalls { $param => $mainMethod }" q"..$mods object $name extends $newTemplate"
}
case _ => abort("@main can be annotation of object only")
}
}
}
import scala.annotation.{StaticAnnotation, compileTimeOnly}
@compileTimeOnly("@Mappable not expanded")
class Mappable extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
template match {
case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {
val expr = paramss.flatten.map(p => q"${p.name.toString}").zip(paramss.flatten.map{
case param"..$mods $paramname: $atpeopt = $expropt" => paramname
}).map{case (q"$paramName", paramTree) => {
q"${Term.Name(paramName.toString)} -> ${Term.Name(paramTree.toString)}"
}} val resultMap = q"Map(..$expr)" val newBody = body :+ q"""def toMap: Map[String, Any] = $resultMap"""
val newTemplate = template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }" q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
}
}
case _ => throw new Exception("@Mappable can be annotation of class only")
}
}
}
import scala.util.Try
@compileTimeOnly("@RetryOnFailure not expanded")
class RetryOnFailure(repeat: Int) extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mods def $name[..$tparams](...$paramss): $tpeopt = $expr" => {
val q"new $_(${arg})" = this
val repeats = Try(arg.toString.toInt).getOrElse(abort(s"Retry on failure takes number as parameter")) val newCode =
q"""..$mods def $name[..$tparams](...$paramss): $tpeopt = {
import scala.util.Try for( a <- to $repeats){
val res = Try($expr)
if(res.isSuccess){
return res.get
}
} throw new Exception("Method fails after "+$repeats + " repeats")
}
"""
newCode
}
case _ => abort("@RetryOnFailure can be annotation of method only")
}
}
} class TalkingAnimal(voice: String) extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case q"..$mods class $tname[..$tparams] (...$paramss) extends $template" =>
template match {
case template"{ ..$stats } with ..$ctorcalls { $param => ..$body }" => {
val q"new $_(${arg})" = this
val sound = arg.toString()
val animalType = tname.toString()
val newBody = body :+
q""" def sayHello: Unit =
println("Hello, I'm a " + $animalType +
" and my name is " + name + " " + $sound+ "...")
"""
val newTemplate =template"{ ..$stats } with ..$ctorcalls { $param => ..$newBody }"
q"..$mods class $tname[..$tparams] (...$paramss) extends $newTemplate"
}
}
case _ => abort("Error: expanding TalkingAnimal!")
}
}
}

试运行代码:

 object MacroAnnotDemo extends App {
@Greetings object Greet {
def add(x: Int, y: Int) = println(x + y)
}
@Greetings object Hi extends AnyRef {} Greet.sayHello("John")
Greet.add(,)
Hi.sayHello("Susana Wang") @Benchmark
def calcPow(x: Double, y: Double) = {
val z = x + y
math.pow(z,z)
} println(calcPow(4.2, 8.9)) @Mappable
case class Car(color: String, model: String, year: Int, owner: String){
def turnOnRadio = {
"playing"
}
} val newCarMap = Car("Silver", "Ford", , "John Doe").toMap
println(newCarMap) object utils {
def methodThrowingException(random: Int): Unit = {
if(random% == ){
throw new Exception(s"throwing exception for ${random}")
}
}
}
import scala.util.Random
@RetryOnFailure() def failMethod[String](): Unit = {
val random = Random.nextInt()
println("Retrying...")
utils.methodThrowingException(random)
} trait Animal {
val name: String
}
@TalkingAnimal("wangwang")
case class Dog(val name: String) extends Animal @TalkingAnimal("miaomiao")
case class Cat(val name: String) extends Animal //@TalkingAnimal("")
//case class Carrot(val name: String)
//Error:(12,2) Annotation TalkingAnimal only apply to Animal inherited! @TalingAnimal
Dog("Goldy").sayHello
Cat("Kitty").sayHello }

Scala Macros - scalamela 1.x,inline-meta annotations的更多相关文章

  1. Scala Macros - 元编程 Metaprogramming with Def Macros

    Scala Macros对scala函数库编程人员来说是一项不可或缺的编程工具,可以通过它来解决一些用普通编程或者类层次编程(type level programming)都无法解决的问题,这是因为S ...

  2. 【Effective C++ 读书笔记】条款02: 尽量以 const, enum, inline 替换 #define

    条款02: 尽量以 const, enum, inline 替换 #define 这个条款或许可以改为“宁可以编译器替换预处理器”. 编译过程: .c文件--预处理-->.i文件--编译--&g ...

  3. Effective C++ 之 Item 2:尽量以 const, enum, inline 替换 #define

    Effective C++ Chapter 1. 让自己习惯C++(Accustoming Yourself to C++) Item 2. 尽量以 const, enum, inline 替换 #d ...

  4. block,inline和inline-block概念和区别

    总体概念 block和inline这两个概念是简略的说法,完整确切的说应该是 block-level elements (块级元素) 和 inline elements (内联元素).block元素通 ...

  5. scala中的面向对象定义类,构造函数,继承

    我们知道scala中一切皆为对象,函数也是对象,数字也是对象,它是一个比java还要面向对象的语言. 定义scala的简单类 class Point (val x:Int, val y:Int) 上面 ...

  6. block,inline和inlinke-block细节对比

    block,inline和inlinke-block细节对比 display:block block元素会独占一行,多个block元素会各自新起一行.默认情况下,block元素宽度自动填满其父元素宽度 ...

  7. block,inline和inline-block概念和区别(转)

    转自  http://www.cnblogs.com/KeithWang/p/3139517.html 总体概念 block和inline这两个概念是简略的说法,完整确切的说应该是 block-lev ...

  8. block,inline,inline-block的区别

    block: 英语翻译过来是“块”意思,就跟小时候玩过的积木方块一样,一块一块往上搭. inline: 英语翻译过来就是“内联”的意思,内联不好理解,我的理解就是行内元素: block和inline都 ...

  9. EC笔记,第一部分:2.尽量以const,enum,inline代替#define

    02.尽量以const,enum,inline代替#define 原因:编译前的预处理会替换宏,所以调试的时候找不到错误 1.const 尽量用const替代常量宏定义 两种特殊情况: (1).常量指 ...

随机推荐

  1. 利用Select2优化@Html.ListBoxFor显示,学会用MultiSelectList

    最近需要用到多选框,Asp.Net MVC自带的@Html.ListBox或@Html.ListBoxFor的显示效果太差,于是找到了Select2进行优化,并正式了解了多选框的操作方法. 首先介绍多 ...

  2. JQuery中的siblings()是什么意思

    jQuery siblings() 方法返回被选元素的所有同胞元素,并且可以使用可选参数来过滤对同胞元素的搜索. 实例演示:点击某个li标签后将其设置为红色,而其所有同胞元素去除红色样式. 1.创建H ...

  3. Atitit.attilax软件研发与项目管理之道

    Atitit.attilax软件研发与项目管理之道 1. 前言4 2. 鸣谢4 3. Genesis 创世记4 4. 软件发展史4 5. 箴言4 6. 使徒行传 4 7. attilax书 4 8. ...

  4. mac下生成ssh keys 并上传github仓储

    使用github仓储需要本机生成一个公钥key 添加到自己的git账户SSH keys中   mac 生成方法:   1. 打开终端 输入   ssh-keygen 然后系统提示输入文件保存位置等信息 ...

  5. 自定义ConfigSection

      CCustom configuration section with intelisense

  6. 入园记------我的DBA之路

    今天周一拖着疲惫的身躯 11点才离开公司,回到家估计写完这篇博客就要17号了. 一个人走在回家的路上,很黑,突然很多感触,一个人在北京拼搏,不敢停止学习的脚步,因为只要停下来就会感觉到孤独. 回顾一下 ...

  7. RabbitMQ的安装过程

    原创文章转载请注明出处:@协思, http://zeeman.cnblogs.com 网上一些安装教程都较为繁琐,实际上只需要两个RPM包,几分钟即可完成一台实例部署. 准备下载Erlang包: ht ...

  8. ASP.NET Identity入门系列教程(一) 初识Identity

    摘要 通过本文你将了解ASP.NET身份验证机制,表单认证的基本流程,ASP.NET Membership的一些弊端以及ASP.NET Identity的主要优势. 目录 身份验证(Authentic ...

  9. [NodeJS] Hello World 起步教程

    概述: 做数据,免不了需要展示数据,数据可视化是必须经历的步骤. 本文将提供一个NodeJS的起步教程,是笔者这两天探索的小结. 正文:  1. 为什么使用NodeJS 究竟是以B/S还是C/S的架构 ...

  10. Atitit HTTP 认证机制基本验证 (Basic Authentication) 和摘要验证 (Digest Authentication)attilax总结

    Atitit HTTP认证机制基本验证 (Basic Authentication) 和摘要验证 (Digest Authentication)attilax总结 1.1. 最广泛使用的是基本验证 ( ...