凯时国际注册首页
  咨询电话:18520235354

凯时K88备用网址

一篇入门 — Scala 宏

前情回顾

上一节, 我简单的说了一下反射的基本概念以及运行时反射的用法, 同时简单的介绍了一下编译原理知识, 其中我感觉最为的地方, 就属泛型的几种使用方式了.而最抽象的概念, 就是对于符号和抽象树的这两个概念的理解.

现在回顾一下泛型的几种进阶用法:

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

现在想想, 既然已经有了泛型了, 还要这几个功能干嘛呢? 其实可以类比一下, 之前没有泛型, 而为什么引入泛型呢?

当然是为了代码更好的服用. 想象一下, 本来一个方法没有入参, 但通过参数, 可以减少很多相似代码.

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

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

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

编译时反射

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

Macros 能做什么呢?

直白一点, 宏能够

Code that generates code

还记得上篇文章中, 我们提到的AST(abstract syntax tree, 抽象语法树)吗? Macros 可以利用 compiler plugincompile-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.Contextscala.reflect.macros.whitebox.Context

黑盒例子

import scala.reflect.macros.blackboxobject 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.blackboxobject 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]

记得上一期已经说过的TypeTagClassTag.

scala> val ru = scala.reflect.runtime.universeru @ 6d657803: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@6d657803scala> 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 boundbar: [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 = { ??? }}

看着眼熟不? 没错, 和ScalaList[+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之前, 我们先了解一下什么是Names, Names在官网解释是:

Names are simple wrappers for strings.

只是一个简单的字符串包装器, 也就是把字符串包装起来, Names有两个子类, 分别是TermNameTypeName, 将一个字符串用两个子类包装起来, 就可以使用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.connectingimport scala.reflect.macros.whiteboximport scala.language.experimental.macrosimport 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 !") } ??? }}

有几点需要注意的地方:

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

Macro Paradise

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