Def 宏

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

实验性

Eugene Burmako

Def 宏自 2.10.0 版本起作为 Scala 的一项实验性功能提供。在彻底制定规范之前,部分 def 宏计划在未来某个版本的 Scala 中稳定下来。

更新 本指南针对 Scala 2.10.0 编写,而我们现在已进入 Scala 2.11.x 发布周期,因此文档内容自然已过时。不过,本指南并未过时 - 此处编写的所有内容在 Scala 2.10.x 和 Scala 2.11.x 中仍然适用,因此阅读它会有所帮助。阅读本指南后,请查看 准引号宏包 的文档,以熟悉极大地简化宏编写的最新开发。然后,关注 我们的宏研讨会 以获取更深入的示例,可能是个好主意。

直觉

这是一个典型的宏定义

def m(x: T): R = macro implRef

乍一看,宏定义等同于普通函数定义,除了它们的正文以外,正文以条件关键字 macro 开头,后面跟一个可能限定的标识符,该标识符引用静态宏实现方法。

如果在类型检查期间,编译器遇到宏 m(args) 的应用,它将通过调用相应的宏实现方法来展开该应用,其中参数表达式 args 的抽象语法树作为参数。宏实现的结果是另一个抽象语法树,它将在调用点内联,并依次进行类型检查。

以下代码片段声明了一个宏定义 assert,它引用了宏实现 Asserts.assertImpl(assertImpl 的定义如下)

def assert(cond: Boolean, msg: Any) = macro Asserts.assertImpl

然后,调用 assert(x < 10, "limit exceeded") 将在编译时导致调用

assertImpl(c)(<[ x < 10 ]>, <[ “limit exceeded” ]>)

其中 c 是一个上下文参数,它包含编译器在调用站点收集的信息,而其他两个参数是表示两个表达式 x < 10limit exceeded 的抽象语法树。

在此文档中,<[ expr ]> 表示表示表达式 expr 的抽象语法树。此符号在 Scala 语言的建议扩展中没有对应符号。实际上,语法树将从 trait scala.reflect.api.Trees 中的类型构造,而上述两个表达式将如下所示

Literal(Constant("limit exceeded"))

Apply(
  Select(Ident(TermName("x")), TermName("$less"),
  List(Literal(Constant(10)))))

以下是 assert 宏的可能实现

import scala.reflect.macros.Context
import scala.language.experimental.macros

object Asserts {
  def raise(msg: Any) = throw new AssertionError(msg)
  def assertImpl(c: Context)
    (cond: c.Expr[Boolean], msg: c.Expr[Any]) : c.Expr[Unit] =
   if (assertionsEnabled)
      <[ if (!cond) raise(msg) ]>
      else
      <[ () ]>
}

如示例所示,宏实现采用多个参数列表。首先是一个参数,类型为 scala.reflect.macros.Context。接下来是一系列参数,这些参数具有与宏定义参数相同的名字。但是,如果原始宏参数的类型为 T,则宏实现参数的类型为 c.Expr[T]Expr[T]Context 中定义的一个类型,它包装了类型为 T 的抽象语法树。 assertImpl 宏实现的结果类型也是一个包装的树,类型为 c.Expr[Unit]

还要注意,宏被认为是实验性和高级功能,因此要编写宏,您需要启用它们。通过 import scala.language.experimental.macros 以每个文件为基础或通过 -language:experimental.macros(提供编译器开关)以每个编译为基础来执行此操作。但是,您的用户不需要启用任何内容 - 宏看起来像普通方法,并且可以使用普通方法,而无需任何编译器开关或其他配置。

通用宏

宏定义和宏实现都可以是通用的。如果宏实现具有类型参数,则必须在宏定义的主体中明确给出实际类型参数。实现中的类型参数可能带有 WeakTypeTag 上下文界限。在这种情况下,描述在应用程序站点实例化的实际类型参数的相应类型标记将在宏展开时传递。

以下代码片段声明了一个宏定义 Queryable.map,它引用了一个宏实现 QImpl.map

class Queryable[T] {
 def map[U](p: T => U): Queryable[U] = macro QImpl.map[T, U]
}

object QImpl {
 def map[T: c.WeakTypeTag, U: c.WeakTypeTag]
        (c: Context)
        (p: c.Expr[T => U]): c.Expr[Queryable[U]] = ...
}

现在考虑一个类型为 Queryable[String] 的值 q 和一个宏调用

q.map[Int](s => s.length)

该调用扩展为以下反射宏调用

QImpl.map(c)(<[ s => s.length ]>)
   (implicitly[WeakTypeTag[String]], implicitly[WeakTypeTag[Int]])

一个完整的示例

本节提供了一个 printf 宏的端到端实现,它在编译时验证并应用格式字符串。为了简单起见,讨论中使用了控制台 Scala 编译器,但如下所述,Maven 和 sbt 也支持宏。

编写宏从宏定义开始,它表示宏的外观。宏定义是一个普通函数,其签名中可以包含任何内容。不过,它的主体只不过是对实现的引用。如上所述,要定义宏,需要导入 scala.language.experimental.macros 或启用一个特殊的编译器开关 -language:experimental.macros

import scala.language.experimental.macros
def printf(format: String, params: Any*): Unit = macro printf_impl

宏实现必须对应于使用它的宏定义(通常只有一个,但也可能有多个)。简而言之,宏定义签名中每个类型为 T 的参数都必须对应于宏实现签名中类型为 c.Expr[T] 的参数。完整的规则列表相当复杂,但这永远不是问题,因为如果编译器不满意,它将在错误消息中打印它期望的签名。

import scala.reflect.macros.Context
def printf_impl(c: Context)(format: c.Expr[String], params: c.Expr[Any]*): c.Expr[Unit] = ...

编译器 API 在 scala.reflect.macros.Context 中公开。它最重要的部分反射 API 可通过 c.universe 访问。通常导入 c.universe._,因为它包含许多常规使用的函数和类型

import c.universe._

首先,宏需要解析提供的格式字符串。宏在编译时运行,因此它们在树上操作,而不是在值上操作。这意味着 printf 宏的格式参数将是编译时文字,而不是类型为 java.lang.String 的对象。这也意味着以下代码不适用于 printf(get_format(), ...),因为在这种情况下 format 不是字符串文字,而是表示函数应用程序的 AST。

val Literal(Constant(s_format: String)) = format.tree

典型的宏(此宏也不例外)需要创建表示 Scala 代码的 AST(抽象语法树)。要详细了解 Scala 代码的生成,请参阅反射概述。除了创建 AST 外,下面提供的代码还操纵类型。请注意,我们如何获取与 IntString 相对应的 Scala 类型。上面链接的反射概述详细介绍了类型操纵。代码生成最后一步将所有生成的代码合并到 Block 中。请注意对 reify 的调用,它提供了创建 AST 的快捷方式。

val evals = ListBuffer[ValDef]()
def precompute(value: Tree, tpe: Type): Ident = {
  val freshName = TermName(c.fresh("eval$"))
  evals += ValDef(Modifiers(), freshName, TypeTree(tpe), value)
  Ident(freshName)
}

val paramsStack = Stack[Tree]((params map (_.tree)): _*)
val refs = s_format.split("(?<=%[\\w%])|(?=%[\\w%])") map {
  case "%d" => precompute(paramsStack.pop, typeOf[Int])
  case "%s" => precompute(paramsStack.pop, typeOf[String])
  case "%%" => Literal(Constant("%"))
  case part => Literal(Constant(part))
}

val stats = evals ++ refs.map(ref => reify(print(c.Expr[Any](ref).splice)).tree)
c.Expr[Unit](Block(stats.toList, Literal(Constant(()))))

下面的代码段表示 printf 宏的完整定义。要遵循此示例,请创建一个空目录,并将代码复制到名为 Macros.scala 的新文件中。

import scala.reflect.macros.Context
import scala.collection.mutable.{ListBuffer, Stack}

object Macros {
  def printf(format: String, params: Any*): Unit = macro printf_impl

  def printf_impl(c: Context)(format: c.Expr[String], params: c.Expr[Any]*): c.Expr[Unit] = {
    import c.universe._
    val Literal(Constant(s_format: String)) = format.tree

    val evals = ListBuffer[ValDef]()
    def precompute(value: Tree, tpe: Type): Ident = {
      val freshName = TermName(c.fresh("eval$"))
      evals += ValDef(Modifiers(), freshName, TypeTree(tpe), value)
      Ident(freshName)
    }

    val paramsStack = Stack[Tree]((params map (_.tree)): _*)
    val refs = s_format.split("(?<=%[\\w%])|(?=%[\\w%])") map {
      case "%d" => precompute(paramsStack.pop, typeOf[Int])
      case "%s" => precompute(paramsStack.pop, typeOf[String])
      case "%%" => Literal(Constant("%"))
      case part => Literal(Constant(part))
    }

    val stats = evals ++ refs.map(ref => reify(print(c.Expr[Any](ref).splice)).tree)
    c.Expr[Unit](Block(stats.toList, Literal(Constant(()))))
  }
}

要使用 printf 宏,请在同一目录中创建另一个文件 Test.scala,并将以下代码放入其中。请注意,使用宏与调用函数一样简单。它也不需要导入 scala.language.experimental.macros

object Test extends App {
  import Macros._
  printf("hello %s!", "world")
}

宏学的一个重要方面是单独编译。为了执行宏扩展,编译器需要可执行形式的宏实现。因此,宏实现需要在主编译之前编译,否则您可能会看到以下错误

~/Projects/Kepler/sandbox$ scalac -language:experimental.macros Macros.scala Test.scala
Test.scala:3: error: macro implementation not found: printf (the most common reason for that is that
you cannot use macro implementations in the same compilation run that defines them)
pointing to the output of the first phase
  printf("hello %s!", "world")
        ^
one error found

~/Projects/Kepler/sandbox$ scalac Macros.scala && scalac Test.scala && scala Test
hello world!

提示和技巧

使用命令行 Scala 编译器使用宏

前一节介绍了此方案。简而言之,使用 scalac 的单独调用编译宏及其用法,一切应该都能正常工作。如果您使用 REPL,那就更好了,因为 REPL 在单独的编译运行中处理每一行,因此您将能够定义宏并立即使用它。

使用 Maven 或 sbt 使用宏

本指南中的演练使用了最简单的可能的命令行编译,但宏也适用于 Maven 和 sbt 等构建工具。查看 https://github.com/scalamacros/sbt-examplehttps://github.com/scalamacros/maven-example 了解端到端示例,但简而言之,您只需要知道两件事

  • 宏在库依赖项中需要 scala-reflect.jar。
  • 单独编译限制要求将宏放在单独的项目中。

在 Intellij IDEA 中使用宏

已知在 Intellij IDEA 中宏可以正常工作,前提是它们被移至一个单独的项目。

调试宏

调试宏(即驱动宏展开的逻辑)相当简单。由于宏是在编译器中展开的,因此您需要做的就是使用调试器运行编译器。为此,您需要:1) 将 Scala 主目录中的 lib 目录中的所有 (!) 库(包括诸如 scala-library.jarscala-reflect.jarscala-compiler.jar 等 jar 文件)添加到调试配置的类路径中,2) 将 scala.tools.nsc.Main 设置为入口点,3) 为 JVM 提供 -Dscala.usejavacp=true 系统属性(非常重要!),4) 将编译器的命令行参数设置为 -cp <path to the classes of your macro> Test.scala,其中 Test.scala 代表包含要展开的宏调用的测试文件。完成所有这些操作后,您应该能够在宏实现中放置一个断点并启动调试器。

工具中真正需要特殊支持的是调试宏展开的结果(即宏生成的代码)。由于此代码从未手动编写,因此您无法在那里设置断点,并且您将无法逐步执行它。Intellij IDEA 团队可能会在某个时候在其调试器中添加对此的支持,但目前调试宏展开的唯一方法是诊断打印:-Ymacro-debug-lite(如下所述),它打印出宏发出的代码,以及 println 来跟踪生成代码的执行。

检查生成的代码

使用 -Ymacro-debug-lite 可以同时看到宏展开生成的代码的伪 Scala 表示和展开的原始 AST 表示。两者都有其优点:前者对于表面分析很有用,而后者对于细粒度调试非常宝贵。

~/Projects/Kepler/sandbox$ scalac -Ymacro-debug-lite Test.scala
typechecking macro expansion Macros.printf("hello %s!", "world") at
source-C:/Projects/Kepler/sandbox\Test.scala,line-3,offset=52
{
  val eval$1: String = "world";
  scala.this.Predef.print("hello ");
  scala.this.Predef.print(eval$1);
  scala.this.Predef.print("!");
  ()
}
Block(List(
ValDef(Modifiers(), TermName("eval$1"), TypeTree().setType(String), Literal(Constant("world"))),
Apply(
  Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("print")),
  List(Literal(Constant("hello")))),
Apply(
  Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("print")),
  List(Ident(TermName("eval$1")))),
Apply(
  Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("print")),
  List(Literal(Constant("!"))))),
Literal(Constant(())))

宏抛出未处理的异常

如果宏抛出未处理的异常会发生什么?例如,让我们通过提供无效输入来使 printf 宏崩溃。如打印输出所示,没有发生任何严重的事情。编译器会保护自己免受行为不端的宏的影响,打印堆栈跟踪的相关部分,并报告错误。

~/Projects/Kepler/sandbox$ scala
Welcome to Scala version 2.10.0-20120428-232041-e6d5d22d28 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_25).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import Macros._
import Macros._

scala> printf("hello %s!")
<console>:11: error: exception during macro expansion:
java.util.NoSuchElementException: head of empty list
        at scala.collection.immutable.Nil$.head(List.scala:318)
        at scala.collection.immutable.Nil$.head(List.scala:315)
        at scala.collection.mutable.Stack.pop(Stack.scala:140)
        at Macros$$anonfun$1.apply(Macros.scala:49)
        at Macros$$anonfun$1.apply(Macros.scala:47)
        at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:237)
        at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:237)
        at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:34)
        at scala.collection.mutable.ArrayOps.foreach(ArrayOps.scala:39)
        at scala.collection.TraversableLike$class.map(TraversableLike.scala:237)
        at scala.collection.mutable.ArrayOps.map(ArrayOps.scala:39)
        at Macros$.printf_impl(Macros.scala:47)

              printf("hello %s!")
                    ^

报告警告和错误

与用户交互的规范方法是通过 scala.reflect.macros.FrontEnds 的方法。 c.error 报告编译错误, c.warning 发出警告, c.abort 报告错误并终止宏的执行。

scala> def impl(c: Context) =
  c.abort(c.enclosingPosition, "macro has reported an error")
impl: (c: scala.reflect.macros.Context)Nothing

scala> def test = macro impl
defined term macro test: Any

scala> test
<console>:32: error: macro has reported an error
              test
              ^

请注意,目前报告工具不支持 SI-6910 中所述的每个位置的多个警告或错误。这意味着每个位置只会报告第一个错误或警告,而其他错误或警告将丢失(即使较早报告了这些错误或警告,错误也会胜过警告)。

编写更大的宏

当宏实现的代码大到足以保证模块化超出实现方法的主体时,显然需要携带上下文参数,因为大多数感兴趣的东西都依赖于上下文的路径。

其中一种方法是编写一个采用类型为 Context 的参数的类,然后将宏实现拆分为该类的系列方法。这是自然而简单的,只是很难正确实现。这是一个典型的编译错误。

scala> class Helper(val c: Context) {
     | def generate: c.Tree = ???
     | }
defined class Helper

scala> def impl(c: Context): c.Expr[Unit] = {
     | val helper = new Helper(c)
     | c.Expr(helper.generate)
     | }
<console>:32: error: type mismatch;
 found   : helper.c.Tree
    (which expands to)  helper.c.universe.Tree
 required: c.Tree
    (which expands to)  c.universe.Tree
       c.Expr(helper.generate)
                     ^

此代码段中的问题在于路径相关的类型不匹配。Scala 编译器不理解 impl 中的 cHelper 中的 c 是同一个对象,即使该帮助器是使用原始 c 构建的。

幸运的是,编译器只需要一点轻推就能弄清楚发生了什么。其中一种可能的方法是使用细化类型(下面的示例是最简单的想法应用;例如,还可以编写从 ContextHelper 的隐式转换,以避免显式实例化并简化调用)。

scala> abstract class Helper {
     | val c: Context
     | def generate: c.Tree = ???
     | }
defined class Helper

scala> def impl(c1: Context): c1.Expr[Unit] = {
     | val helper = new { val c: c1.type = c1 } with Helper
     | c1.Expr(helper.generate)
     | }
impl: (c1: scala.reflect.macros.Context)c1.Expr[Unit]

另一种方法是在显式类型参数中传递上下文的标识。请注意 Helper 的构造函数如何使用 c.type 来表示 Helper.c 与原始 c 相同。Scala 的类型推断无法自行解决这个问题,因此我们需要帮助它。

scala> class Helper[C <: Context](val c: C) {
     | def generate: c.Tree = ???
     | }
defined class Helper

scala> def impl(c: Context): c.Expr[Unit] = {
     | val helper = new Helper[c.type](c)
     | c.Expr(helper.generate)
     | }
impl: (c: scala.reflect.macros.Context)c.Expr[Unit]

此页面的贡献者