准引号

卫生

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

Denys Shabalin、Eugene Burmako 实验性

卫生概念已在 Scheme 的宏研究中得到广泛普及。如果代码生成器确保常规代码和生成代码之间不存在名称冲突,防止意外捕获标识符,则称该代码生成器具有卫生性。正如大量经验报告所示,卫生对于代码生成非常重要,因为名称绑定问题通常不明显,并且缺乏卫生性可能会以微妙的方式表现出来。

Racket 等复杂的宏系统具有机制,无需宏编写者做出任何努力即可使宏具有卫生性。在 Scala 中,我们没有自动卫生性 - 我们的两个代码生成工具(使用宏的编译时代码生成和使用工具箱的运行时代码生成)都要求程序员手动处理卫生性。你必须知道如何解决卫生性缺失问题,这就是本节的主题。

防止常规代码和生成代码之间出现名称冲突意味着两件事。首先,我们必须确保,无论我们在其中放置生成代码的上下文如何,其含义都不会改变(引用透明性)。其次,我们必须确保,无论我们在其中拼接常规代码的上下文如何,其含义都不会改变(通常称为狭义上的卫生性)。让我们通过一系列示例看看为此可以采取哪些措施。

引用透明性

引用透明性意味着准引号应记住其定义时的词法上下文。例如,如果准引号的定义位置提供了导入,则应使用这些导入来解析准引号中的名称。遗憾的是,目前并非如此,下面是一个示例

scala> import collection.mutable.Map

scala> def typecheckType(tree: Tree): Type =
         toolbox.typecheck(tree, toolbox.TYPEmode).tpe

scala> typecheckType(tq"Map[_, _]") =:= typeOf[Map[_, _]]
false

scala> typecheckType(tq"Map[_, _]") =:= typeOf[collection.immutable.Map[_, _]]
true

这里我们可以看到对 Map 的不合格引用不尊重我们的自定义导入,而是解析为默认 collection.immutable.Map。如果宏中的引用未完全限定,则可能会出现类似问题。

// ---- MyMacro.scala ----
package example

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

object MyMacro {
  def wrapper(x: Int) = { println(s"wrapped x = $x"); x }
  def apply(x: Int): Int = macro impl
  def impl(c: Context)(x: c.Tree) = {
    import c.universe._
    q"wrapper($x)"
  }
}

// ---- Test.scala ----
package example

object Test extends App {
  def wrapper(x: Int) = x
  MyMacro(2)
}

如果我们编译宏及其用法,我们将看到当应用程序运行时不会调用 println。这将发生,因为在宏扩展之后,Test.scala 将如下所示

// Expanded Test.scala
package example

object Test extends App {
  def wrapper(x: Int) = x
  wrapper(2)
}

并且 wrapper 将解析为 example.Test.wrapper 而不是预期的 example.MyMacro.wrapper。为了避免引用透明性陷阱,可以使用两种可能的解决方法

  1. 完全限定所有引用。即,我们可以调整宏的实现为

    def impl(c: Context)(x: c.Tree) = {
      import c.universe._
      q"_root_.example.MyMacro.wrapper($x)"
    }
    

    重要的是以 _root_ 开头,否则如果在宏的使用位置重新定义 example,仍然会出现名称冲突。

  2. 取消引用符号,而不是使用普通标识符。即,我们可以手动解析对 wrapper 的引用

    def impl(c: Context)(x: c.Tree) = {
      import c.universe._
      val myMacro = symbolOf[MyMacro.type].asClass.module
      val wrapper = myMacro.info.member(TermName("wrapper"))
      q"$wrapper($x)"
    }
    

狭义上的卫生

“狭义上的卫生”意味着准引号不应干扰取消引用到其中的树的绑定。例如,如果取消引用到宏扩展中的宏参数最初引用了封闭词法上下文中的一些变量,那么无论为该宏扩展生成了什么代码,该引用都应在宏扩展后保持有效。遗憾的是,我们没有自动工具来确保这一点,这可能会导致意外情况

scala> val originalTree = q"val x = 1; x"
originalTree: universe.Tree = ...

scala> toolbox.eval(originalTree)
res1: Any = 1

scala> val q"$originalDefn; $originalRef" = originalTree
originalDefn: universe.Tree = val x = 1
originalRef: universe.Tree = x

scala> val generatedTree = q"$originalDefn; { val x = 2; println(x); $originalRef }"
generatedTree: universe.Tree = ...

scala> toolbox.eval(generatedTree)
2
res2: Any = 2

在该示例中,val x = 2 的定义隐藏了原始树中建立的从 xval x = 1 的绑定,从而更改了生成代码中 originalRef 的语义。在此简单示例中,隐藏很容易遵循,但在精细的宏中,它很容易失控。

为了避免这种情况,有一种经过早期 Lisp 实战检验的解决方法;有一个函数可以创建要在生成代码中使用的唯一名称。在 Lisp 术语中,它被称为 gensym,而在 Scala 中,我们称之为 freshName。准引号在这里特别好,因为它们允许将生成的名称直接取消引用到生成的代码中。

不过,我们的 API 有点混乱。在编译时和运行时环境中,都有一个内部 API internal.reificationSupport.{ freshTermName, freshTypeName },但是,只有在编译时,才有一个漂亮的公共外观,称为 c.freshName。我们计划在 Scala 2.12 中修复此问题。

scala> val xfresh = universe.internal.reificationSupport.freshTermName("x$")
xfresh: universe.TermName = x$1

scala> val generatedTree = q"$originalDefn; { val $xfresh = 2; println($xfresh); $originalRef }"
generatedTree: universe.Tree = ...

scala> toolbox.eval(generatedTree)
2
res2: Any = 1

此页面的贡献者