了解Scala 宏

前情回顾

了解Scala反射介绍了反射的基本概念以及运行时反射的用法, 同时简单的介绍了一下编译原理知识, 其中我感受最为的地方, 就属泛型的几种使用方式了.javascript

而最抽象的概念, 就是对于符号和抽象树的这两个概念的理解.html

如今回顾一下泛型的几种进阶用法:java

  • 上界 <:
  • 下界 >:
  • 视界 <%
  • 边界 :
  • 协变 +T
  • 逆变 -T

如今想一想, 既然已经有了泛型了, 还要这几个功能干吗呢? 其实能够类比一下, 以前没有泛型, 而为何引入泛型呢?python

固然是为了代码更好的服用. 想象一下, 原本一个方法没有入参, 但经过参数, 能够减小不少类似代码.程序员

同理, 泛型是什么, generics. 又叫什么, 类型参数化. 原本方法的入参只能接受一种类型的参数, 加入泛型后, 能够处理多种类型的入参.sql

顺着这条线接着往下想, 有了逆变和协变, 咱们让泛型的包装类也有了类继承关系, 有了继承的层级关系, 方法的处理能力又会大大增长.编程

泛型, 并不神奇, 只是省略了一系列代码, 并且引入泛型还会致使泛型擦除, 以及一系列的隐患. 而类型擦除其实也是为了兼容更早的语言, 咱们一筹莫展.
但泛型在设计上实现的数据和逻辑分离, 却能够大大提升程序代码的简洁性和可读性, 并提供可能的编译时类型转换安全检测功能. 因此在可使用泛型的地方咱们仍是推荐的.api

Scala Macros对scala函数库编程人员来讲是一项不可或缺的编程工具,能够经过它来解决一些用普通编程或者类层次编程(type level programming)都没法解决的问题,这是由于Scala Macros能够直接对程序进行修改。安全

说到对程序进行修改,几个概念必定要先理解,"编译期"和"运行期",Java也有一个能够修改程序的功能,你们必定用过,就是反射.不是在运行期,编译器也不知道接下来会发生什么,会执行哪些代码(这就是==动态性==).ruby

而scala是java的衍生语言,天然也有反射,并且它还有一种更高级的反射,就是编译时反射,它就是宏.

什么是宏

通常说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(一般是字符串)如何根据预约义的规则转换成对应的输出(一般是字符串,或者是类,方法等)。这种替换在预编译时进行,称做宏展开。

经过上面的定义,感受和C的宏概念差很少.但C的宏只不过是一段语法的替换,然而Scala的宏却能够经过表达式树控制一节代码(类,或者方法)的生成。得到了控制代码的执行顺序(见惰性计算和非限制函数)的能力,使得新建立的语法结构与语言内建的语法结构不可区分。

宏,从程序抽象的角度来看,可能不太容易调试和维护,可是可以很强大的固定咱们的设计. 同时使用宏可以==大量==的减小样板代码.好比Scala的assertrequire就是使用宏实现的.

宏出现的意义

  • 编译期元编程
  • 更完善的错误检查

编译期元编程

什么是元编程?

百度词条的一句话:

元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其余程序(或者自身)做为它们的数据,==或者在运行时完成部分本应在编译时完成的工做==。不少状况下与手工编写所有代码相比工做效率更高。编写元程序的语言称之为元语言,被操做的语言称之为目标语言。==一门语言同时也是自身的元语言的能力称之为反射==。

元编程是用来产生代码的程序,操纵代码的程序,在运行时建立和修改代码而非编程时,这种程序叫作元程序。而编写这种程序就叫作元编程。

因此,元编程技术在多种编程语言中均可以使用,但更多的仍是被应用于动态语言中,由于动态语言提供了更多的在运行时将代码视为数据进行操纵的能力。
虽然静态语言也支持元编程(反射机制),可是仍然没有诸如Ruby这样的更趋动态性的语言那么透明,这是由于静态语言在运行时其代码和数据是分布在两个层次上的。

最后能够理解为,元编程就是程序能够操做更小的粒度和动做.

更完善的错误检查

引自知乎https://www.zhihu.com/question/27685977/answer/38014170

首先思考一个问题:若是你的应用程序有bug,那么你但愿在什么状况下发现呢?

  • 编译时:这是最理想的状态,若是一个bug能够经过编译器检查出来,那么程序员能够在第一时间发现问题,基本上就是一边写一边fix。这也正是静态编译型语言的强大优点。
  • 单元测试:没有那么理想可是也不差。每写完一段code跑一下测试,看看有没有新的bug出来。对于scala来讲,如今的工具链已经不错了,左屏sbt > ~test,右屏写代码惬意得很。
  • 运行时:这个就比较糟糕了。运行时才报错意味着你得首先打包部署,这个时间开销一般就比较大,并且在许多公司,你还要时不时的解决环境问题,非常让人抓狂。

而Scala的宏,就是能够将一些运行期才会出现的错误,在编译器暴露出来.

编译时反射

上篇文章已经介绍过, 编译器反射也就是在Scala的表现形式, 就是咱们本篇的重点 宏(Macros).

Macros 能作什么呢?

直白一点, 宏可以

Code that generates code

还记得上篇文章中, 咱们提到的AST(abstract syntax tree, 抽象语法树)吗? Macros 能够利用 compiler plugin 在 compile-time 操做 AST, 从而实现一些为因此为的...任性操做

因此, 能够理解宏就是一段在编译期运行的代码, 若是咱们能够合理的利用这点, 就能够将一些代码提早执行, 这意味着什么, 更早的(compile-time)发现错误, 从而避免了 run-time错误. 还有一个不大不小的好处, 就是能够减小方法调用的堆栈开销.

是否是很吸引人, 好, 开始Macros的盛宴.

黑盒宏和白盒宏

黑盒和白盒的概念, 就不作过多介绍了. 而Scala既然引用了这两个单词来描述宏, 那么二者区别也就显而易见了. 固然, 这两个是新概念, 在2.10以前, 只有一种宏, 也就是白盒宏的前身.

官网描述以下:
Macros that faithfully follow their type signatures are called blackbox macros as their implementations are irrelevant to understanding their behaviour (could be treated as black boxes).
Macros that can't have precise signatures in Scala's type system are called whitebox macros (whitebox def macros do have signatures, but these signatures are only approximations).

我怕每一个人的理解不同, 因此先贴出了官网的描述, 而个人理解呢, 就是咱们指定好返回类型的Macros就是黑盒宏, 而咱们虽然指定返回值类型, 甚至是以c.tree定义返回值类型, 而更加细致的具体类型, 即真正的返回类型能够在宏中实现的, 咱们称为白盒宏.

可能仍是有点绕哈, 我举个例子吧. 在此以前, 先把两者的位置说一下:

2.10

  • scala.reflect.macros.Context

2.11 +

  • scala.reflect.macros.blackbox.Context
  • scala.reflect.macros.whitebox.Context

黑盒例子

import scala.reflect.macros.blackbox object Macros { def hello: Unit = macro helloImpl def helloImpl(c: blackbox.Context): c.Expr[Unit] = { import c.universe._ c.Expr { Apply( Ident(TermName("println")), List(Literal(Constant("hello!"))) ) } } }

可是要注意, 黑盒宏的使用, 会有四点限制, 主要方面是

  • 类型检查
  • 类型推到
  • 隐式推到
  • 模式匹配

这里我不细说了, 有兴趣能够看看官网: https://docs.scala-lang.org/overviews/macros/blackbox-whitebox.html

白盒例子

import scala.reflect.macros.blackbox object Macros { def hello: Unit = macro helloImpl def helloImpl(c: blackbox.Context): c.Tree = { import c.universe._ c.Expr(q"""println("hello!")""") } }

Using macros is easy, developing macros is hard.

了解了Macros的两种规范以后, 咱们再来看看它的两种用法, 一种和C的风格很像, 只是在编译期将宏展开, 减小了方法调用消耗. 还有一种用法, 我想你们更熟悉, 就是注解, 将一个宏注解标记在一个类, 方法, 或者成员上, 就能够将所见的代码, 经过AST变成everything, 不过, 请不要变的太离谱.

Def Macros

方法宏, 其实以前的代码中, 已经见识过了, 没什么稀奇, 但刚才的例子仍是比较简单的, 若是咱们要传递一个参数, 或者泛型呢?

看下面例子:

object Macros {
    def hello2[T](s: String): Unit = macro hello2Impl[T] def hello2Impl[T](c: blackbox.Context)(s: c.Expr[String])(ttag: c.WeakTypeTag[T]): c.Expr[Unit] = { import c.universe._ c.Expr { Apply( Ident(TermName("println")), List( Apply( Select( Apply( Select( Literal(Constant("hello ")), TermName("$plus") ), List( s.tree ) ), TermName("$plus") ), List( Literal(Constant("!")) ) ) ) ) } } }

和以前的不一样之处, 暴露的方法hello2主要在于多了参数s和泛型T, 而hello2Impl实现也多了两个括号

  • (s: c.Expr[String])
  • (ttag: c.WeakTypeTag[T])

咱们来一一讲解

c.Expr

这是Macros的表达式包装器, 里面放置着类型String, 为何不能直接传String呢?
固然是不能够了, 由于宏的入参只接受Expr, 调用宏传入的参数也会默认转为Expr.

这里要注意, 这个(s: c.Expr[String])的入参名必须等于hello2[T](s: String)的入参名

WeakTypeTag[T]

记得上一期已经说过的TypeTag 和 ClassTag.

scala> val ru = scala.reflect.runtime.universe
ru @ 6d657803: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@6d657803

scala> def foo[T: ru.TypeTag] = implicitly[ru.TypeTag[T]]
foo: [T](implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T] scala> foo[Int] res0 @ 7eeb8007: reflect.runtime.universe.TypeTag[Int] = TypeTag[Int] scala> foo[List[Int]] res1 @ 7d53ccbe: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]

这都没有问题, 可是若是我传递一个泛型呢, 好比这样:

scala> def bar[T] = foo[T] // T is not a concrete type here, hence the error <console>:26: error: No TypeTag available for T def bar[T] = foo[T] ^

没错, 对于不具体的类型(泛型), 就会报错了, 必须让T有一个边界才能够调用, 好比这样:

scala> def bar[T: TypeTag] = foo[T] // to the contrast T is concrete here
 // because it's bound by a concrete tag bound bar: [T](implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T]

但, 有时咱们没法为泛型提供边界, 好比在本章的Def Macros中, 这怎么办? 不要紧, 杨总说过:

任何计算机问题均可以经过加一层中间件解决.

因此, Scala引入了一个新的概念 => WeakTypeTag[T], 放在TypeTag之上, 以后能够

scala> def foo2[T] = weakTypeTag[T] foo2: [T]=> reflect.runtime.universe.WeakTypeTag[T]

无须边界, 照样使用, 而TypeTag就不行了.

scala> def foo[T] = typeTag[T] <console>:15: error: No TypeTag available for T def foo[T] = typeTag[T]

有兴趣请看
https://docs.scala-lang.org/overviews/reflection/typetags-manifests.html

Apply

在前面的例子中, 咱们屡次看到了Apply(), 这是作什么的呢?
咱们能够理解为这是一个AST构建函数, 比较好奇的我看了下源码, 搜打死乃.

class ApplyExtractor{ def apply(fun: Tree, args: List[Tree]): Apply = { ??? } }

看着眼熟不? 没错, 和Scala 的List[+A]的构建函数相似, 一个延迟建立函数. 好了, 先理解到这.

Ident

定义, 能够理解为Scala标识符的构建函数.

Literal(Constant("hello "))

文字, 字符串构建函数

Select

选择构建函数, 选择的什么呢? 答案是一切, 不管是选择方法, 仍是选择类. 咱们能够理解为.这个调用符. 举个例子吧:

scala> showRaw(q"scala.Some.apply") res2: String = Select(Select(Ident(TermName("scala")), TermName("Some")), TermName("apply"))

还有上面的例子:
"hello ".$plus(s.tree)

Apply(
    Select( Literal(Constant("hello ")), TermName("$plus") ), List( s.tree ) )

源码以下:

class SelectExtractor { def apply(qualifier: Tree, name: Name): Select = { ??? } }

TermName("$plus")

理解TermName以前, 咱们先了解一下什么是NamesNames在官网解释是:

Names are simple wrappers for strings.

只是一个简单的字符串包装器, 也就是把字符串包装起来, Names有两个子类, 分别是TermName 和 TypeName, 将一个字符串用两个子类包装起来, 就可使用Select 在tree中进行查找, 或者组装新的tree.

官网地址

宏插值器

刚刚就为了实现一个如此简单的功能, 就写了那么巨长的代码, 若是如此的话, 即使Macros 功能强大, 也不易推广Macros. 所以Scala又引入了一个新工具 => Quasiquotes

Quasiquotes 大大的简化了宏编写的难度, 并极大的提高了效率, 由于它让你感受写宏就像写scala代码同样.

一样上面的功能, Quasiquotes实现以下:

object Macros {
    def hello2[T](s: String): Unit = macro hello2Impl[T] def hello2Impl[T](c: blackbox.Context)(s: c.Expr[String])(ttag: c.WeakTypeTag[T]): c.Expr[Unit] = { import c.universe._ val tree = q"""println("hello " + ${s.tree} + "!")""" c.Expr(tree) } }

q""" ??? """ 就和 s""" ??? """r""" ??? """ 同样, 可使用$引用外部属性, 方便进行逻辑处理.

Macros ANNOTATIONS

宏注释, 就和咱们在Java同样, 下面是我写的一个例子:
对于以class修饰的类, 咱们也像case class修饰的类同样, 完善toString()方法.

package com.pharbers.macros.common.connecting

import scala.reflect.macros.whitebox
import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}

@compileTimeOnly("enable macro paradis to expand macro annotations") final class ToStringMacro extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro ToStringMacro.impl } object ToStringMacro { def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val class_tree = annottees.map(_.tree).toList match { case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$parents { $self => ..$stats }" :: Nil => val params = paramss.flatMap { params => val q"..$trees" = q"..$params" trees } val fields = stats.flatMap { params => val q"..$trees" = q"..$params" trees.map { case q"$mods def toString(): $tpt = $expr" => q"" case x => x }.filter(_ != EmptyTree) } val total_fields = params ++ fields val toStringDefList = total_fields.map { case q"$mods val $tname: $tpt = $expr" => q"""${tname.toString} + " = " + $tname""" case q"$mods var $tname: $tpt = $expr" => q"""${tname.toString} + " = " + $tname""" case _ => q"" }.filter(_ != EmptyTree) val toStringBody = if(toStringDefList.isEmpty) q""" "" """ else toStringDefList.reduce { (a, b) => q"""$a + ", " + $b""" } val toStringDef = q"""override def toString(): String = ${tpname.toString()} + "(" + $toStringBody + ")"""" q""" $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$parents { $self => ..$stats $toStringDef } """ case _ => c.abort(c.enclosingPosition, "Annotation @One2OneConn can be used only with class") } c.Expr[Any](class_tree) } }

compileTimeOnly

非强制的, 但建议加上. 官网解释以下:

It is not mandatory, but is recommended to avoid confusion. Macro annotations look like normal annotations to the vanilla Scala compiler, so if you forget to enable the macro paradise plugin in your build, your annotations will silently fail to expand. The @compileTimeOnly annotation makes sure that no reference to the underlying definition is present in the program code after typer, so it will prevent the aforementioned situation from happening.

StaticAnnotation

继承自StaticAnnotation的类, 将被Scala解释器标记为注解类, 以注解的方式使用, 因此不建议直接生成实例, 加上final修饰符.

macroTransform

def macroTransform(annottees: Any*): Any = macro ToStringMacro.impl

对于使用@ToStringMacro修饰的代码, 编译器会自动调用macroTransform方法, 该方法的入参, 是annottees: Any*, 返回值是Any, 主要是由于Scala缺乏更细致的描述, 因此使用这种笼统的方式描述能够接受一切类型参数.
而方法的实现, 和Def Macro同样.

impl

def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ ??? }

到了Macros的具体实现了. 这里其实和Def Macro也差很少. 但对于须要传递参数的宏注解, 须要按照下面的写法:

final class One2OneConn[C](param_name: String) extends StaticAnnotation {  def macroTransform(annottees: Any*): Any = macro One2OneConn.impl } object One2OneConn {  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {  import c.universe._   // 匹配当前注解, 得到参数信息  val (conn_type, conn_name) = c.prefix.tree match {  case q"new One2OneConn[$conn_type]($conn_name)" =>  (conn_type.toString, conn_name.toString.replace("\"", ""))  case _ => c.abort(c.enclosingPosition, "Annotation @One2OneConn must provide conn_type and conn_name !")  }   ???  } }

有几点须要注意的地方:

  1. 宏注解只能操做当前自身注解, 和定义在当前注解之下的注解, 对于以前的注解, 由于已经展开, 因此已经不能操做了.
  2. 若是宏注解生成多个结果, 例如既要展开注解标识的类, 还要直接生成类实例, 则返回结果须要以块(Block)包起来.
  3. 宏注释必须使用白盒宏.

Macro Paradise

Scala 推出了一款插件, 叫作Macro Paradise(宏天堂), 能够帮助开发者控制带有宏的Scala代码编译顺序, 同时还提供调试功能, 这里不作过多介绍, 有兴趣的能够查看官网: Macro Paradise

 

宏的编译过程?

Scala是如何编译宏的呢?

引用自http://www.javashuo.com/article/p-mnkbrolf-my.html
image

明白了上面的流程以后,咱们出个栗子:

object modules {
    greeting("john") } object mmacros { def greeting(person: String): Unit = macro greetingMacro def greetingMacro(c: Context)(person: c.Expr[String]): c.Expr[Unit] = { import c.universe._ println("compiling greeting ...") val now = reify {new Date().toString} reify { println("Hello " + person.splice + ", the time is: " + new Date().toString) } } }

以上代码的执行逻辑以下:
image

注意编译器在运算greetingMacro时会以AST方式将参数person传入。因为在编译modules对象时须要运算greetingMacro函数,因此greetingMacro函数乃至整个mmacros对象必须是已编译状态,这就意味着modules和mmacros必须分别在不一样的源代码文件里,并且还要确保在编译modules前先完成对mmacros的编译.

编写宏实现

其实宏的使编写并不难,api已经帮咱们作好了一切,咱们只要关注如何使用获取宏参数和宏的返回值便可.
上面栗子中的代码,greetingMacro方法就是一个最简单的宏实现,代码以下:

def greetingMacro(c: Context)(person: c.Expr[String]): c.Expr[Unit] = { import c.universe._ println("compiling greeting ...") val now = reify {new Date().toString} reify { println("Hello " + person.splice + ", the time is: " + new Date().toString) } }

可是想要实现更多的功能,还须要更加深刻的学习Scala的宏和表达式树.

宏语法糖

1.implicit macor (隐式宏)

官方文档

开局出个栗子

trait Showable[T] { 
 def show(x: T): String } def show[T](x: T)(implicit s: Showable[T]) = s.show(x) implicit object IntShowable extends Showable[Int] {  def show(x: Int) = x.toString } show(42) // return "42" show("42") // compilation error

能够调用成功show(),主要由于名称空间存在Showable的子类IntShowable,而且是implicit object,这个implicit object的做用上一篇已经讲过了,就不说了.

上面代码,乍一看还能够,可是若是扩展起来就不是很舒服了,若是要让show("42")也可用,咱们就须要添加以下代码:

implicit object StringShowable extends Showable[String] { def show(x: String) = x }

2.宏注解 Macro Annotations ==> @compileTimeOnly("")

官方文档

开局处个栗子,能够自动为case classclass在编译时生成一个名为TempLog的方法.

import scala.reflect.macros.Context
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly

@compileTimeOnly("temp log print") class AnPrint(msg: Any*) extends StaticAnnotation { def macroTransform(annottees : Any*) : Any = macro AnPrintMacroImpl.impl }

官网栗子,咱们的代码也比较常见,继承了StaticAnnotation,表示这是一个注解类,有兴趣的朋友能够看看上一期文章.

主要说的是上面

@compileTimeOnly("temp log print")

官网解释

First of all, note the @compileTimeOnly annotation. It is not mandatory, but is recommended to avoid confusion

首先,这不是强制性的,即使不写,也会被编译器自动扩展上.但仍是建议加上避免混乱.

而后是宏的具体实现,以下:

object AnPrintMacroImpl {
    def impl(c : whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val tree = annottees.map(_.tree).toList.head val (className, fields, parents, body) = tree match{ case q"case class $className(..$fields) extends ..$parents { ..$body }" => (className, fields, parents, body) case q"class $className(..$fields) extends ..$parents { ..$body }" => (className, fields, parents, body) } //TempLog val LogDefName = TermName("TempLog") val LogDefImpl = q"""def $LogDefName(sss: Any):Unit=println(" ===> " + sss)""" val out = q""" case class $className(..$fields) extends ..$parents { ..$LogDefImpl ..$body } """ println(showRaw(tree)) c.Expr(out) } }

里面的具体细节,主要是宏将类变成AST,而后利用模式匹配,来解析类信息,以后能够加入本身定义的任何操做,最后用Block封装起来,这样子,一个简单的宏就实现了.

咱们测试一下:

package myTest

@AnPrint("clock") class ccc(val a: String = "aaa", val b: String = "bbb"){ TempLog("init b") } object annotationPrintTest extends App { println("start") val a = new b("aiyou", "wolegequ") a.TempLog("打印我了") println("end") }

注意,这里须要先编译AnPrintMacroImplAnPrint文件,才能够测试经过

打印结果以下:

start
===> init b ===> 打印我了 end
相关文章
相关标签/搜索