在 GitHub 上编辑此页面

在开发宏时启用 -Xcheck-macros scalac 选项标志以进行额外的运行时检查。

多阶段

带引号的表达式

Scala 3 中的多阶段编程使用引号 '{..} 来延迟,即阶段,代码的执行,并使用拼接 ${..} 来评估和将代码插入引号中。带引号的表达式被键入为 Expr[T],其中协变类型参数为 T。使用这两个概念很容易编写静态安全的代码生成器。以下示例显示了 $x^n$ 数学运算的一个朴素实现。

import scala.quoted.*
def unrolledPowerCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
  if n == 0 then '{ 1.0 }
  else if n == 1 then x
  else '{ $x * ${ unrolledPowerCode(x, n-1) } }
'{
  val x = ...
  ${ unrolledPowerCode('{x}, 3) } // evaluates to: x * x * x
}

引号和拼接是彼此的对偶。对于类型为 T 的任意表达式 x,我们有 ${'{x}} = x,对于类型为 Expr[T] 的任意表达式 e,我们有 '{${e}} = e

抽象类型

引号可以使用类型类 Type[T] 来处理泛型和抽象类型。引用泛型或抽象类型 T 的引号需要在隐式作用域中提供给定的 Type[T]。以下示例展示了如何使用上下文绑定(: Type)对 T 进行注释以提供隐式 Type[T],或等效的 using Type[T] 参数。

import scala.quoted.*
def singletonListExpr[T: Type](x: Expr[T])(using Quotes): Expr[List[T]] =
  '{ List[T]($x) } // generic T used within a quote

def emptyListExpr[T](using Type[T], Quotes): Expr[List[T]] =
  '{ List.empty[T] } // generic T used within a quote

如果找不到其他实例,则使用默认的 Type.of[T]。以下示例隐式使用 Type.of[String]Type.of[Option[U]]

val list1: Expr[List[String]] =
  singletonListExpr('{"hello"}) // requires a given `Type[Sting]`
val list0: Expr[List[Option[T]]] =
  emptyListExpr[Option[U]] // requires a given `Type[Option[U]]`

Type.of[T] 方法是一项基本操作,编译器会专门处理它。如果类型 T 是静态已知的,或者如果 T 包含我们有隐式 Type[Ui] 的其他一些类型 Ui,它将提供隐式类型。在示例中,Type.of[String] 具有静态已知类型,而 Type.of[Option[U]] 需要范围内的隐式 Type[U]

引用上下文

我们还使用给定的 Quotes 实例跟踪当前引用上下文。要创建引用 '{..},我们需要给定的 Quotes 上下文,该上下文应作为上下文参数 (using Quotes) 传递给函数。每个拼接都会在拼接范围内提供一个新的 Quotes 上下文。因此,引用和拼接可以被视为具有以下签名的函数,但具有特殊语义。

def '[T](x: T): Quotes ?=> Expr[T] // def '[T](x: T)(using Quotes): Expr[T]

def $[T](x: Quotes ?=> Expr[T]): T

带问号 ?=> 的 lambda 是一个上下文函数;它是一个 lambda,它隐式获取其参数并在 lambda 实现中隐式提供它。Quotes 用于各种目的,这些目的将在涵盖这些主题时提到。

引用值

提升

虽然无法使用跨阶段的本地变量持久性,但可以将它们提升到下一阶段。为此,我们提供了 Expr.apply 方法,它可以获取一个值并将其提升到该值的引用表示中。

val expr1plus1: Expr[Int] = '{ 1 + 1 }

val expr2: Expr[Int] = Expr(1 + 1) // lift 2 into '{ 2 }

虽然它在类型方面看起来类似于 '{ 1 + 1 },但 Expr(1 + 1) 的语义完全不同。Expr(1 + 1) 不会分阶段或延迟任何计算;参数被计算为一个值,然后提升到引用中。该引用将包含将在下一阶段创建此值的副本的代码。Expr 是多态的,并且可以通过 ToExpr 类型类进行用户扩展。

trait ToExpr[T]:
  def apply(x: T)(using Quotes): Expr[T]

我们可以使用 given 定义实现 ToExpr,该定义将该定义添加到范围内的隐式定义中。在以下示例中,我们展示了如何为任何可提升类型 `T` 实现 ToExpr[Option[T]]

given OptionToExpr[T: Type: ToExpr]: ToExpr[Option[T]] with
  def apply(opt: Option[T])(using Quotes): Expr[Option[T]] =
    opt match
      case Some(x) => '{ Some[T]( ${Expr(x)} ) }
      case None => '{ None }

基本类型的 ToExpr 必须在系统中作为基本操作实现。在我们的例子中,我们使用反射 API 来实现它们。

从引用中提取值

为了能够使用 unrolledPowerCode 方法生成优化的代码,宏实现 powerCode 需要首先确定作为参数 n 传递的参数是否是一个已知的常量值。这可以通过使用我们库实现中的 Expr.unapply 提取器进行取消提升来实现,该提取器仅在 n 是一个引用常量时匹配并提取其值。

def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
  n match
    case Expr(m) => // it is a constant: unlift code n='{m} into number m
      unrolledPowerCode(x, m)
    case _ => // not known: call power at run-time
      '{ power($x, $n) }

或者,可以使用 n.value 方法获取带有值的 Option[Int],或使用 n.valueOrAbort 直接获取值。

def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
  // emits an error message if `n` is not a constant
  unrolledPowerCode(x, n.valueOrAbort)

Expr.unapplyvalue 的所有变体都是多态的,并且可以通过给定的 FromExpr 类型类进行用户扩展。

trait FromExpr[T]:
  def unapply(x: Expr[T])(using Quotes): Option[T]

我们可以使用 given 定义来实现 FromExpr,就像我们对 ToExpr 所做的那样。必须在系统中将基本类型的 FromExpr 实现为基本操作。在我们的示例中,我们使用反射 API 来实现它们。要为非基本类型实现 FromExpr,我们使用引用模式匹配(例如 OptionFromExpr)。

宏和多阶段编程

该系统使用相同的引用抽象来支持多阶段宏和运行时多阶段编程。

多阶段宏

我们可以概括拼接抽象以表示宏。宏由未嵌套在任何引用中的顶级拼接组成。从概念上讲,拼接的内容比程序早一个阶段进行评估。换句话说,在编译程序时评估内容。宏生成的代码替换程序中的拼接。

def power2(x: Double): Double =
  ${ unrolledPowerCode('x, 2) } // x * x

内联宏

由于在程序中间使用拼接不如调用函数那么符合人体工程学;我们向宏的最终用户隐藏了暂存机制。我们有统一的方法来调用宏和普通函数。为此,我们限制顶级拼接的使用,使其仅出现在内联方法中[^1][^2]。

// inline macro definition
inline def powerMacro(x: Double, inline n: Int): Double =
  ${ powerCode('x, 'n) }

// user code
def power2(x: Double): Double =
  powerMacro(x, 2) // x * x

只有当代码内联到 power2 中时,才会对宏进行评估。内联时,代码等效于 power2 的先前定义。使用内联方法的一个后果是,宏的任何参数或返回类型都不必提及 Expr 类型;这向最终用户隐藏了元编程的所有方面。

避免使用完整解释器

在评估顶级拼接时,编译器需要解释拼接中的代码。为整个语言提供解释器非常棘手,让解释器高效运行更具挑战性。为了避免需要一个完整的解释器,我们可以在拼接上施加以下限制,以简化对顶级拼接中代码的评估。

  • 顶级拼接必须包含对已编译静态方法的单个调用。
  • 函数的参数是文字常量、带引号的表达式(参数)、对类型参数的 Type.of 调用以及对 Quotes 的引用。

特别是,这些限制禁止在顶级拼接中使用拼接。这种拼接需要多个解释阶段,这会造成不必要的低效。

编译阶段

宏实现(即在顶级拼接中调用的方法)可以来自任何预编译库。这为编译过程的各个阶段提供了明确的区别。考虑在不同库中定义的以下 3 个源文件。

// Macro.scala
def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] = ...
inline def powerMacro(x: Double, inline n: Int): Double =
  ${ powerCode('x, 'n) }
// Lib.scala (depends on Macro.scala)
def power2(x: Double) =
  ${ powerCode('x, '{2}) } // inlined from a call to: powerMacro(x, 2)
// App.scala  (depends on Lib.scala)
@main def app() = power2(3.14)

一种语法可视化方式是将应用程序放入一个延迟应用程序编译的引用中。然后可以将应用程序依赖项放在包含引用应用程序的外部引用中,并且我们对依赖项的依赖项重复此操作。

'{ // macro library (compilation stage 1)
  def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
    ...
  inline def powerMacro(x: Double, inline n: Int): Double =
    ${ powerCode('x, 'n) }
  '{ // library using macros (compilation stage 2)
    def power2(x: Double) =
      ${ powerCode('x, '{2}) } // inlined from a call to: powerMacro(x, 2)
    '{ power2(3.14) /* app (compilation stage 3) */ }
  }
}

为了使系统更通用,我们允许在定义宏的项目中调用宏,但有一些限制。例如,将 Macro.scalaLib.scala 一起编译到同一个库中。为此,我们不遵循更简单的语法模型,而是依赖于源文件的语义信息。在编译源时,如果我们检测到对尚未编译的宏的调用,我们会将此源的编译延迟到下一个编译阶段。在示例中,我们会延迟 Lib.scala 的编译,因为它包含对 powerCode 的编译时调用。编译阶段会重复进行,直到所有源都编译完成,或者无法取得任何进展。如果无法取得任何进展,则宏的定义和使用之间存在循环依赖关系。我们还需要检测在运行时宏是否依赖于尚未编译的源。通过执行宏并检查尚未编译的类的 JVM 链接错误来检测这些错误。

运行时多阶段编程

请参阅 运行时多阶段编程

安全性

多阶段编程在设计上是静态安全的和跨阶段安全的。

静态安全性

卫生

所有标识符名称都解释为对引用中上下文中相应变量的符号引用。因此,在评估引用时,不可能意外地将引用重新绑定到具有相同文本名称的新变量。

类型良好

如果引用类型良好,则生成的代码类型良好。这是跟踪每个表达式类型的简单结果。Expr[T] 只能从包含类型为 T 的表达式的引用创建。相反,Expr[T] 只能拼接在需要类型 T 的位置。如前所述,Expr 在其类型参数中协变。这意味着 Expr[T] 可以包含 T 的子类型的表达式。当拼接在需要类型 `T` 的位置时,这些表达式也具有有效的类型。

跨阶段安全性

级别一致性

我们将某个代码的暂存级别定义为引号数减去包围该代码的拼接数。局部变量必须在同一暂存级别中定义和使用。

永远无法从较低暂存级别访问局部变量,因为它尚未存在。

def badPower(x: Double, n: Int): Double =
  ${ unrolledPowerCode('x, n) } // error: value of `n` not known yet

在宏和跨平台可移植性(即,在某台计算机上编译但可能在另一台计算机上执行的宏)的上下文中,我们无法支持局部变量的跨阶段持久性。因此,局部变量只能在我们的系统中完全相同的暂存级别访问。

def badPowerCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
  // error: `n` potentially not available in the next execution environment
  '{ power($x, n) }

对于全局定义(如 unrolledPowerCode),规则略有不同。可以生成包含对全局定义的引用的代码,如 '{ power(2, 4) } 中所示。这是一种有限形式的跨阶段持久性,不会妨碍跨平台可移植性,其中我们引用 power 的已编译代码。每个编译步骤将暂存级别降低一个,同时保留全局定义。因此,我们可以在 ${ unrolledPowerCode('x, 2) } 中的宏(如 unrolledPowerCode)中引用已编译的定义。

我们可以将级别一致性总结为两条规则

  • 局部变量只能在其定义的同一暂存级别使用
  • 全局变量可以在任何暂存级别使用

类型一致性

由于 Scala 使用类型擦除,因此泛型类型将在运行时擦除,因此在任何后续阶段都会擦除。为了确保引用泛型类型 T 的任何引号表达式不会丢失其所需的信息,我们需要一个给定的 Type[T] 处于作用域内。Type[T] 将类型未擦除的表示形式传递到下一阶段。因此,在比其定义更高的暂存级别使用的任何泛型类型都需要其 Type

作用域挤出

在拼接的内容中,可以引用外部引号中定义的局部变量的引号。如果在拼接中使用此引号,则变量将处于作用域中。但是,如果引号以某种方式挤出拼接之外,则变量可能不再处于作用域中。可以使用副作用(如可变状态和异常)来挤出引号表达式。以下示例显示了如何使用可变状态挤出引号。

var x: Expr[T] = null
'{ (y: T) => ${ x = 'y; 1 } }
x // has value '{y} but y is not in scope

变量可以挤出的第二种方式是通过 run 方法。如果 run 消耗了引号变量引用,则它将不再处于作用域中。结果将引用在下一阶段定义的变量。

'{ (x: Int) => ${ run('x); ... } }
// evaluates to: '{ (x: Int) => ${ x; ... } 1

为了捕获两种作用域扩展场景,我们的系统通过仅允许在引号未从拼接作用域中扩展出来时拼接引号来限制引号的使用。这与级别一致性不同,这是在运行时[^4]而不是编译时进行检查的,以避免使静态类型系统过于复杂。

每个 Quotes 实例都包含一个唯一的作用域标识符,并引用其父作用域,形成一个标识符堆栈。Quotes 作用域的父作用域是用于创建封闭引号的 Quotes 的作用域。顶级拼接和 run 创建新的作用域堆栈。每个 Expr 都知道它是在哪个作用域中创建的。当它被拼接时,我们检查引号作用域是否与拼接作用域相同,或者是否是其父作用域。

分阶段 Lambda

在函数式语言中分阶段执行程序时,有两个基本抽象:分阶段 Lambda Expr[T => U] 和分阶段 Lambda Expr[T] => Expr[U]。第一个是在下一阶段存在的函数,而第二个是在当前阶段存在的函数。通常,有一个机制从 Expr[T => U]Expr[T] => Expr[U] 反之亦然会很方便。

def later[T: Type, U: Type](f: Expr[T] => Expr[U]): Expr[T => U] =
  '{ (x: T) => ${ f('x) } }

def now[T: Type, U: Type](f: Expr[T => U]): Expr[T] => Expr[U] =
  (x: Expr[T]) => '{ $f($x) }

使用引号和拼接可以开箱即用地执行这两个转换。但是,如果 f 是已知的 Lambda 函数,则 '{ $f($x) } 不会就地对 Lambda 进行 Beta 规约。此优化在编译器的稍后阶段执行。不立即规约应用程序可以简化生成代码的分析。不过,可以使用 Expr.betaReduce 方法就地对 Lambda 进行 Beta 规约。

def now[T: Type, U: Type](f: Expr[T => U]): Expr[T] => Expr[U] =
  (x: Expr[T]) => Expr.betaReduce('{ $f($x) })

betaReduce 方法将尽可能对表达式的最外层应用程序进行 Beta 规约(与元数无关)。如果无法对表达式进行 Beta 规约,则它将返回原始表达式。

分阶段构造函数

要在稍后阶段创建新的类实例,我们可以使用工厂方法(通常是 objectapply 方法)创建它们,或者我们可以使用 new 对它们进行实例化。例如,我们可以编写 Some(1)new Some(1),创建相同的值。在 Scala 3 中,如果找不到 apply 方法,使用工厂方法调用符号将回退到 new。在调用工厂方法时,我们遵循通常的分阶段规则。类似地,当我们使用 new C 时,将隐式调用 C 的构造函数,这也遵循通常的分阶段规则。因此,对于任意已知的类 C,我们可以使用 '{ C(...) }'{ new C(...) } 作为构造函数。

分阶段类

引用的代码可以包含任何有效表达式,包括本地类定义。这允许创建具有专门实现的新类。例如,我们可以实现一个新版本的 Runnable,它将执行一些优化的操作。

def mkRunnable(x: Int)(using Quotes): Expr[Runnable] = '{
  class MyRunnable extends Runnable:
    def run(): Unit = ... // generate some custom code that uses `x`
  new MyRunnable
}

引用的类是本地类,其类型无法跳出封闭的引用。该类必须在引用内使用,或者可以使用已知接口(本例中为 Runnable)返回其实例。

引用模式匹配

有时需要分析代码的结构或将代码分解为其子表达式。一个经典的例子是嵌入式 DSL,其中宏知道一组定义,它可以在编译代码时重新解释这些定义(例如,执行优化)。在以下示例中,我们将 powCode 的先前实现扩展为查看 x 以执行进一步优化。

def fusedPowCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
  x match
    case '{ power($y, $m) } => // we have (y^m)^n
      fusedPowCode(y, '{ $n * $m }) // generate code for y^(n*m)
    case _ =>
      '{ power($x, $n) }

子模式

在引用的模式中,$ 将子表达式绑定到表达式 Expr,该表达式可以在该 case 分支中使用。引用模式中 ${..} 的内容是常规 Scala 模式。例如,我们可以在 ${..} 内使用 Expr(_) 模式,仅在它是已知值并提取它时匹配。

def fusedUnrolledPowCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
  x match
    case '{ power($y, ${Expr(m)}) } => // we have (y^m)^n
      fusedUnrolledPowCode(y, n * m) // generate code for y * ... * y
    case _ =>                        //                  ( n*m times )
      unrolledPowerCode(x, n)

这些值提取子模式可以使用 FromExpr 的实例进行多态化。在以下示例中,我们展示了 OptionFromExpr 的实现,它在内部使用 FromExpr[T] 来使用 Expr(x) 模式提取值。

given OptionFromExpr[T](using Type[T], FromExpr[T]): FromExpr[Option[T]] with
  def unapply(x: Expr[Option[T]])(using Quotes): Option[Option[T]] =
    x match
      case '{ Some( ${Expr(x)} ) } => Some(Some(x))
      case '{ None } => Some(None)
      case _ => None

封闭模式

模式可能包含两种引用:全局引用(例如 '{ power(...) } 中对 power 方法的调用),或对模式中定义的绑定的引用(例如 case '{ (x: Int) => x } 中的 x)。从引用中提取表达式时,我们需要确保不会从定义它的作用域中挤出任何变量。

'{ (x: Int) => x + 1 } match
  case '{ (y: Int) => $z } =>
    // should not match, otherwise: z = '{ x + 1 }

在此示例中,我们看到模式不应该匹配。否则,对表达式 z 的任何使用都将包含对 x 的未绑定引用。为了避免任何此类挤出,我们仅在 ${..} 的表达式在模式内的定义下封闭时才匹配。因此,如果表达式未关闭,模式将不匹配。

HOAS 模式

为了允许提取可能包含挤出引用的表达式,我们提供了一个高阶抽象语法 (HOAS) 模式 $f(y)(或 $f(y1,...,yn))。此模式将针对 y eta 展开子表达式,并将其绑定到 f。lambda 参数将替换可能已被挤出的变量。

'{ ((x: Int) => x + 1).apply(2) } match
  case '{ ((y: Int) => $f(y): Int).apply($z: Int) } =>
    // f may contain references to `x` (replaced by `$y`)
    // f = '{ (y: Int) => $y + 1 }
    Expr.betaReduce('{ $f($z)}) // generates '{ 2 + 1 }

HOAS 模式 $x(y1,...,yn) 仅当它不包含对模式中定义的变量的引用(这些变量不在集合 y1,...,yn 中)时,才会匹配表达式。换句话说,如果表达式仅包含对模式中定义的变量(这些变量在 y1,...,yn 中)的引用,则该模式将匹配。请注意,HOAS 模式 $x() 在语义上等同于闭合模式 $x

类型变量

表达式可能包含静态未知的类型。例如,Expr[List[Int]] 可能包含 list.map(_.toInt),其中 list 是某种类型的 List。为了涵盖所有可能的情况,我们需要明确地对所有可能的类型(List[Int]List[Int => Int] 等)匹配 list。这是一组无限的类型,因此也是一组无限的模式情况。即使我们知道特定程序可能使用所有可能的类型,我们最终仍可能得到数量难以管理的情况。为了克服这一点,我们在引用模式中引入了类型变量,它将匹配任何类型。

在以下示例中,我们展示了类型变量 tu 如何匹配对列表执行 map 的所有可能的连续调用。在引用模式中,以小写字母命名的类型被标识为类型变量。这遵循与普通模式中使用的类型变量相同的表示法。

def fuseMapCode(x: Expr[List[Int]]): Expr[List[Int]] =
  x match
    case '{ ($ls: List[t]).map[u]($f).map[Int]($g) } =>
      '{ $ls.map($g.compose($f)) }
    ...

fuseMapCode('{ List(1.2).map(f).map(g) }) // '{ List(1.2).map(g.compose(f)) }
fuseMapCode('{ List('a').map(h).map(i) }) // '{ List('a').map(i.compose(h))  }

变量 fg 分别推断为类型 Expr[t => u]Expr[u => Int]。随后,我们可以推断 $g.compose($f) 的类型为 Expr[t => Int],这是 $ls.map(..) 参数的类型。

类型变量是将被擦除的抽象类型;这意味着要引用它们,在第二个引用中我们需要给定的 Type[t]Type[u]。引用模式将隐式提供这些给定的类型。在运行时,当模式匹配时,将知道 tu 的类型,并且 Type[t]Type[u] 将包含表达式中的精确类型。

由于 Expr 是协变的,因此表达式的静态已知类型可能不是实际类型。类型变量还可用于恢复表达式的精确类型。

def let(x: Expr[Any])(using Quotes): Expr[Any] =
  x match
    case '{ $x: t } =>
      '{ val y: t = $x; y }

let('{1}) // will return a `Expr[Any]` that contains an `Expr[Int]]`

还可以在一个模式中多次引用同一个类型变量。

case '{ $x: (t, t) } =>

虽然我们可以在模式的中间定义类型变量,但它们的标准形式是将它们定义为一个type,其名称在模式的开头使用小写字母。

case '{ type t; $x: t } =>

这有点冗长,但有一些表现力优势,例如允许对变量定义边界。

case '{ type t >: List[Int] <: Seq[Int]; $x: t } =>

类型模式

只可能有一个类型而没有该类型的表达式。为了能够检查类型,我们引入了带引号的类型模式case '[..] =>。它的工作方式与带引号的模式相同,但仅限于包含类型。类型变量可用于带引号的类型模式中以提取类型。

def empty[T: Type](using Quotes): Expr[T] =
  Type.of[T] match
    case '[String] => '{ "" }
    case '[List[t]] => '{ List.empty[t] }
    case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] }
    ...

Type.of[T]用于在范围内调用给定的Type[T]实例,它等效于summon[Type[T]]

可以使用类型变量上的适当类型边界来匹配高阶类型。

def empty[K <: AnyKind : Type](using Quotes): Type[?] =
  Type.of[K] match
    case '[type f[X]; f] => Type.of[f]
    case '[type f[X <: Int, Y]; f] => Type.of[f]
    case '[type k <: AnyKind; k ] => Type.of[k]

类型测试和转换

重要的是要注意,对Expr的实例检查和转换,例如isInstanceOf[Expr[T]]asInstanceOf[Expr[T]],只会检查该实例是否属于Expr类,但无法检查T参数。这些情况将在编译时发出警告,但如果忽略它们,则可能导致意外行为。

这些操作可以在系统中得到正确支持。对于简单的类型测试,可以使用ExprisExprOf[T]方法来检查它是否是该类型的实例。类似地,可以使用asExprOf[T]将表达式转换为给定类型。这些操作使用给定的Type[T]来解决类型擦除问题。

子表达式转换

该系统提供了一种转换表达式所有子表达式的机制。当我们要转换的子表达式在表达式中很深时,这很有用。如果表达式包含无法使用带引号的模式(例如局部类定义)匹配的子表达式,这也是必需的。

trait ExprMap:
  def transform[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T]
  def transformChildren[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T] =
    ...

用户可以扩展ExprMap特征并实现transform方法。此接口很灵活,可以实现自上而下、自下而上或其他转换。

object OptimizeIdentity extends ExprMap:
  def transform[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T] =
    transformChildren(e) match // bottom-up transformation
      case '{ identity($x) } => x
      case _ => e

transformChildren方法实现为一个基元,它知道如何访问所有直接子表达式并在每个子表达式上调用transform。传递给transform的类型是该子表达式在其表达式中的预期类型。例如,在'{ val x: Option[Int] = Some(1); ...}中转换Some(1)时,类型将是Option[Int]而不是Some[Int]。这意味着我们可以安全地将Some(1)转换为None

分阶段隐式调用

在使用summon调用隐式参数时,我们将在当前范围内找到给定的实例。可以通过显式分阶段来使用summon获取分阶段隐式参数。在以下示例中,我们可以将隐式的Ordering[T]作为Expr[Ordering[T]]传递给宏作为其实现。然后,我们可以拼接它并隐式地将其提供给下一阶段。

inline def treeSetFor[T](using ord: Ordering[T]): Set[T] =
  ${ setExpr[T](using 'ord) }

def setExpr[T:Type](using ord: Expr[Ordering[T]])(using Quotes): Expr[Set[T]] =
  '{ given Ordering[T] = $ord; new TreeSet[T]() }

我们将其作为隐式 Expr[Ordering[T]] 传递,因为可能存在可以隐式传递它的中间方法。

另一种方法是在宏被调用时的作用域中调用隐式值。使用 Expr.summon 方法,我们可以得到一个包含隐式实例的可选表达式。这提供了有条件地搜索隐式实例的能力。

def summon[T: Type](using Quotes): Option[Expr[T]]
inline def setFor[T]: Set[T] =
  ${ setForExpr[T] }

def setForExpr[T: Type]()(using Quotes): Expr[Set[T]] =
  Expr.summon[Ordering[T]] match
    case Some(ord) =>
      '{ new TreeSet[T]()($ord) }
    case _ =>
      '{ new HashSet[T] }

更多详细信息

  • 规范
  • Scala 3 中的可扩展元编程[^1]

[^1]: Scala 3 中的可扩展元编程 [^2]: 元编程的语义保留内联 [^3]: 在 Scala 3 Dotty 项目中实现 https://github.com/lampepfl/dotty。sbt 库依赖项 "org.scala-lang" %% "scala3-staging" % scalaVersion.value [^4]: 使用 -Xcheck-macros 编译器标志