宏注释

语言
此文档页面特定于 Scala 2 中提供的功能,这些功能在 Scala 3 中已被删除或替换为其他功能。除非另有说明,此页面中的所有代码示例都假设您使用的是 Scala 2。

宏天堂

尤金·布尔马科

宏注释在 Scala 2.13 中可用,带有 -Ymacro-annotations 标志,以及从 Scala 2.10.x 到 Scala 2.12.x 的宏天堂插件。如果您使用那些较旧的 Scala 版本,请按照 “宏天堂” 页面上的说明下载并使用我们的编译器插件。

请注意,宏天堂插件既用于编译宏注释,也用于扩展宏注释,这意味着您的用户还必须在构建中添加宏天堂才能使用您的宏注释。但是,在宏注释扩展后,生成的代码将不再引用宏天堂,并且在编译时或运行时都不需要它。

演练

宏注解将文本抽象提升到定义级别。使用 Scala 识别的宏注解任何顶级或嵌套定义,都将允许其展开,可能展开为多个成员。与之前版本的宏天堂不同,2.0 中的宏注解是正确的,因为它们:1) 不仅适用于类和对象,而且适用于任意定义,2) 允许类扩展修改甚至创建伴随对象。这为代码生成领域开辟了许多新可能性。

在本演练中,我们将编写一个愚蠢但非常有用的宏,除了记录注解对象外,它什么都不做。作为第一步,我们定义一个继承 StaticAnnotation 并定义 macroTransform 宏的注解(macroTransform 名称和 annottees: Any* 的宏签名很重要,因为它们告诉宏引擎封闭的注解是宏注解)。

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

@compileTimeOnly("enable macro paradise to expand macro annotations")
class identity extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro ???
}

首先,请注意 @compileTimeOnly 注解。它不是强制性的,但建议使用它以避免混淆。对于香草 Scala 编译器来说,宏注解看起来像普通注解,因此,如果您忘记在构建中启用宏天堂插件,您的注解将静默地无法展开。 @compileTimeOnly 注解确保在类型检查器之后,程序代码中不存在对底层定义的任何引用,因此它将防止上述情况发生。

现在, macroTransform 宏应该获取一个未类型化的注解对象列表(在签名中,它们的类型表示为 Any,因为 Scala 中没有更好的概念)并生成一个或多个结果(单个结果可以按原样返回,多个结果必须包装在 Block 中,因为反射 API 中没有更好的概念)。

此时,您可能想知道。单个注解对象和单个结果是可以理解的,但多对多映射意味着什么?有几条规则指导此过程

  1. 如果一个类被注解,并且它有一个伴随类,那么两者都将传递到宏中。(但反之则不然 - 如果一个对象被注解,并且它有一个伴随类,则只有对象本身被展开)。
  2. 如果类、方法或类型成员的参数被注解,那么它将展开其所有者。首先是注解对象,然后是所有者,然后是伴随对象,如上一条规则所指定。
  3. 注解对象可以展开为任意类型的任意数量的树,然后编译器将透明地用宏的输出树替换输入树。
  4. 如果一个类展开为一个类和一个具有相同名称的模块,它们将成为伴随类。这样,即使没有显式声明伴随对象,也可以为类生成伴随对象。
  5. 顶级扩展必须保留注释者的数量、其风格和其名称,唯一例外是类可能扩展到同名类加同名模块,在这种情况下,它们会根据之前的规则自动成为伴随类。

以下是 identity 注释宏的可能实现。逻辑有点复杂,因为它需要考虑 @identity 应用于值或类型参数的情况。请原谅我们的低技术解决方案,但我们尚未将此样板封装在帮助器中,因为编译器插件无法轻松更改标准库。(顺便说一下,此样板可以通过合适的注释宏进行抽象,我们很可能会在以后的某个时间点提供此类宏)。

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

@compileTimeOnly("enable macro paradise to expand macro annotations")
class identity extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro identityMacro.impl
}

object identityMacro {
  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    val inputs = annottees.map(_.tree).toList
    val (annottee, expandees) = inputs match {
      case (param: ValDef) :: (rest @ (_ :: _)) => (param, rest)
      case (param: TypeDef) :: (rest @ (_ :: _)) => (param, rest)
      case _ => (EmptyTree, inputs)
    }
    println((annottee, expandees))
    val outputs = expandees
    c.Expr[Any](Block(outputs, Literal(Constant(()))))
  }
}
示例代码 打印输出
@identity class C (<empty>, List(class C))
@identity class D; object D (<empty>, List(class D, object D))
class E; @identity object E (<empty>, List(object E))
def twice[@identity T]
(@identity x: Int) = x * 2
(type T, List(def twice))
(val x: Int, List(def twice))

本着 Scala 宏的精神,宏注释尽可能无类型以保持灵活性,尽可能有类型以保持有用性。一方面,宏注释者是无类型的,以便我们可以更改其签名(例如类成员列表)。但另一方面,所有风格的 Scala 宏的要点是与类型检查器集成,而宏注释也不例外。在扩展期间,我们可以拥有所有可能获得的类型信息(例如,我们可以针对周围程序进行反射,或在封闭上下文中执行类型检查/隐式查找)。

黑盒与白盒

宏注释必须是 白盒。如果您将宏注释声明为 黑盒,它将不起作用。

此页面的贡献者