一篇入门 — Scala 宏

前情回顾

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

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

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

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

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

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

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

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

编译时反射

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

Macros 能作什么呢?

直白一点, 宏可以插件

Code that generates codescala

还记得上篇文章中, 咱们提到的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.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]

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

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 = {
        ???
    }
}

看着眼熟不? 没错, 和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.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

相关文章
相关标签/搜索