Scala 3 中的宏

反射

语言
此文档页面特定于 Scala 3,可能涵盖 Scala 2 中不可用的新概念。除非另有说明,此页面中的所有代码示例均假定你正在使用 Scala 3。

反射 API 提供了对代码结构更复杂、更全面的视图。它提供了类型化抽象语法树及其属性(如类型、符号、位置和注释)的视图。

API 可用于宏以及 检查 TASTy 文件

如何使用 API

反射 API 在类型 Quotes 中定义为 reflect。实际实例取决于当前范围,其中使用引号或引号模式匹配。因此,每个宏方法都会收到 Quotes 作为附加参数。由于 Quotes 是上下文相关的,因此要访问其成员,我们需要命名参数或调用它。标准库中的以下定义详细说明了访问它的规范方法

package scala.quoted

transparent inline def quotes(using inline q: Quotes): q.type = q

我们可以使用 scala.quoted.quotes 来导入当前范围内的 Quotes

import scala.quoted.* // Import `quotes`, `Quotes`, and `Expr`

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....
  val tree: Tree = ...
  ...

这将导入 API 的所有类型和模块(带有扩展方法)。

如何浏览 API

可以在 scala.quoted.Quotes.reflectModule 的 API 文档 中找到完整的 API。不幸的是,在此阶段,此自动生成的文档不容易浏览。

页面上最重要的元素是层次结构树,它提供了 API 中类型子类型关系的综合概述。对于树中的每个类型 Foo

  • 特征 FooMethods 包含类型 Foo 上可用的方法
  • 特征 FooModule 包含对象 Foo 上可用的静态方法。最值得注意的是,构造函数(apply/copy)和 unapply 方法,后者提供了模式匹配所需的提取器
  • 对于所有类型 Upper,使得 Foo <: UpperUpperMethods 中定义的方法在 Foo 上也可用

例如,TypeBoundsTypeRepr 的子类型,表示形式为 T >: L <: U 的类型树:类型 TL 的超类型,是 U 的子类型。在 TypeBoundsMethods 中,你会找到方法 lowhi,它们允许你访问 LU 的表示。在 TypeBoundsModule 中,你会找到 unapply 方法,它允许你编写

def f(tpe: TypeRepr) =
  tpe match 
    case TypeBounds(l, u) =>

由于 TypeBounds <: TypeReprTypeReprMethods 中定义的所有方法在 TypeBounds 值上可用

def f(tpe: TypeRepr) =
  tpe match
    case tpe: TypeBounds =>
      val low = tpe.low
      val hi  = tpe.hi

与 Expr/Type 的关系

Expr 和 Term

表达式(Expr[T])可以看作是 Term 的包装器,其中 T 是该项的静态已知类型。下面,我们使用扩展方法 asTerm 将表达式转换为项。此扩展方法仅在导入 quotes.reflect.asTerm 后可用。然后,我们使用 asExprOf[Int] 将该项转换回 Expr[Int]。如果该项没有提供的类型(在本例中为 Int),或者该项不是有效的表达式,则此操作将失败。例如,如果方法 fn 采用类型参数,则 Ident(fn) 是一个无效的项,在这种情况下,我们需要一个 Apply(Ident(fn), args)

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.*
  val tree: Term = x.asTerm
  val expr: Expr[Int] = tree.asExprOf[Int]
  expr

Type 和 TypeRepr

类似地,我们还可以将 Type[T] 看作 TypeRepr 的包装器,其中 T 是静态已知的类型。要获取 TypeRepr,我们使用 TypeRepr.of[T],它期望在范围内给定 Type[T](类似于 Type.of[T])。我们还可以使用 asType 方法将其转换回 Type[?]。由于 Type[?] 的类型不是静态已知的,因此我们需要使用存在类型对其进行命名才能使用它。这可以通过使用 '[t] 模式来实现。

def g[T: Type](using Quotes) =
  import quotes.reflect.*
  val tpe: TypeRepr = TypeRepr.of[T]
  tpe.asType match
    case '[t] => '{ val x: t = ${...} }
  ...

符号

TermTypeRepr 的 API 在某种意义上是相对封闭的,方法生成并接受其类型在 API 中定义的值。但是,您可能会注意到标识定义的 Symbol 的存在。

TermTypeRepr(因此 ExprType)都有一个关联的符号。使用 == 比较两个定义,Symbol 使得您可以知道它们是否相同。此外,Symbol 公开并被许多有用的方法使用。例如

  • declaredFieldsdeclaredMethods 允许您迭代符号中定义的字段和成员
  • flags 允许您检查符号的多个属性
  • companionClasscompanionModule 提供了一种跳转到伴随对象/类并返回的方式
  • TypeRepr.baseClasses 返回类型扩展的类的符号列表
  • Symbol.pos 使您可以访问符号定义的位置、定义的源代码,甚至符号定义的文件名
  • 您可以在 SymbolMethods 中找到许多其他内容

转换为 Symbol 并返回

考虑一个名为 val tpe: TypeRepr = ...TypeRepr 类型的实例。然后

  • tpe.typeSymbol 返回由 TypeRepr 表示的类型的符号。给定 Type[T],获取 Symbol 的推荐方法是 TypeRepr.of[T].typeSymbol
  • 对于单例类型,tpe.termSymbol 返回底层对象或值的符号
  • tpe.memberType(symbol) 返回提供的符号的 TypeRepr
  • 在对象 t: Tree 上,t.symbol 返回与树关联的符号。鉴于 Term <: TreeExpr.asTerm.symbol 是获取与 Expr[T] 关联的符号的最佳方法
  • 在对象 sym: Symbol 上,sym.tree 返回与符号关联的 Tree。使用此方法时要小心,因为符号的树可能未定义。请阅读 最佳实践页面 上的更多信息

宏 API 设计

创建执行宏的某些常见逻辑的帮助程序方法或提取器通常很有用。

最简单的方法是仅在签名中提及 ExprTypeQuotes 的方法。在内部,它们可能使用反射,但这不会在方法的使用位置看到。

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.*
  ...

在某些情况下,某些方法可能不可避免地会期望或返回 Treequotes.reflect 中的其他类型。对于这些情况,最佳做法是遵循以下方法签名示例

获取 quotes.reflect.Term 参数的方法

def f(using Quotes)(term: quotes.reflect.Term): String =
  import quotes.reflect.*
  ...

返回 quotes.reflect.Treequotes.reflect.Term 的扩展方法

extension (using Quotes)(term: quotes.reflect.Term)
  def g: quotes.reflect.Tree = ...

匹配 quotes.reflect.Term 的提取器

object MyExtractor:
  def unapply(using Quotes)(x: quotes.reflect.Term) =
    ...
    Some(y)

避免将 Quotes 上下文保存在字段中。字段中的 Quotes 不可避免地会因导致涉及具有不同路径的 Quotes 的错误而使其使用变得更加困难。

通常,这些模式已在使用 Scala 2 方式定义扩展方法或上下文非应用的代码中看到。现在我们有了可以在其他参数之前添加的 given 参数,所有这些旧的解决方法都不再需要了。新的抽象在定义站点和使用站点都使其变得更简单。

调试

运行时检查

表达式 (Expr[T]) 可以看作是 Term 的包装器,其中 T 是该术语的静态已知类型。因此,这些检查将在运行时进行(即宏展开时的编译时)。

建议在开发宏或宏测试时启用 -Xcheck-macros 标志。此标志将启用额外的运行时检查,这些检查将尝试在创建错误格式的树或类型时立即找到它们。

还有 -Ycheck:all 标志,它检查树的格式良好的所有编译器不变量。这些检查通常会因断言错误而失败。

打印树

quotes.reflect 包中类型上的 toString 方法不适合调试,因为它们显示的是内部表示形式,而不是 quotes.reflect 表示形式。在许多情况下,这些情况是相似的,但有时它们可能会导致调试过程出现偏差,因此不应依赖它们。

相反,quotes.reflect.Printers 提供了一组用于调试的有用打印机。值得注意的是,TreeStructureTypeReprStructureConstantStructure 类非常有用。它们将打印树结构,大致遵循匹配它所需的提取器。

val tree: Tree = ...
println(tree.show(using Printer.TreeStructure))

可以在 Tree 上的模式匹配末尾添加此内容,这是最有用的地方之一。

tree match
  case Ident(_) =>
  case Select(_, _) =>
  ...
  case _ =>
    throw new MatchError(tree.show(using Printer.TreeStructure))

这样,如果错过了某个情况,错误将报告一个熟悉的结构,可以复制粘贴以开始修复问题。

如果需要,可以将此打印机设为默认值

  import quotes.reflect.*
  given Printer[Tree] = Printer.TreeStructure
  ...
  println(tree.show)

更多

即将推出

此页面的贡献者