Scala 3 中的宏

Scala 3 宏

语言
此文档页面特定于 Scala 3,可能涵盖 Scala 2 中不可用的新概念。 除非另有说明,否则此页面中的所有代码示例都假设您使用的是 Scala 3。

内联方法 为我们提供了一种优雅的元编程技术,通过在编译时执行某些操作来实现。 但是,有时内联不足以满足需求,我们需要更强大的方法来分析和合成编译时的程序。 宏使我们能够做到这一点:将程序视为数据并对其进行操作。

宏将程序视为值

使用宏,我们可以将程序视为值,这使我们能够在编译时分析和生成它们。

带有类型 T 的 Scala 表达式由类型 scala.quoted.Expr[T] 的实例表示。

我们将在讨论 Quoted CodeReflection 时深入探讨类型 Expr[T] 的细节,以及分析和构建实例的不同方法。现在,只需要知道宏是操作类型 Expr[T] 表达式的元程序。

以下宏实现将在编译时将提供的参数的表达式打印到编译器进程的标准输出中

import scala.quoted.* // imports Quotes, Expr

def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] =
  println(x.show)
  x

打印参数表达式后,我们将原始参数作为类型 Expr[Any] 的 Scala 表达式返回。

正如在关于 Inline 的部分中预示的那样,内联方法为宏定义提供了入口点

inline def inspect(inline x: Any): Any = ${ inspectCode('x) }

所有宏都使用 inline def 定义。此入口点的实现始终具有相同的形状

  • 它们只包含一个 splice ${ ... }
  • splice 包含对实现宏的方法的单个调用(例如 inspectCode)。
  • 对宏实现的调用接收引用的参数(即 'x 而不是 x)和一个上下文 Quotes

我们将在本节和下一节中更深入地探讨这些概念。

调用我们的 inspectinspect(sys error "abort") 会在编译时打印参数表达式的字符串表示

scala.sys.error("abort")

宏和类型参数

如果宏具有类型参数,则实现也需要了解它们。就像 scala.quoted.Expr[T] 表示类型 T 的 Scala 表达式一样,我们使用 scala.quoted.Type[T] 来表示 Scala 类型 T

inline def logged[T](inline x: T): T = ${ loggedCode('x)  }

def loggedCode[T](x: Expr[T])(using Type[T], Quotes): Expr[T] = ...

类型 Type[T] 的实例和上下文 Quotes 会由相应内联方法(即 logged)中的 splice 自动提供,并且可以被宏实现使用。

定义和使用宏

内联和宏之间的一个关键区别在于它们评估的方式。内联通过重写代码并根据编译器已知的规则执行优化来工作。另一方面,宏执行用户编写的代码,该代码生成宏扩展到的代码。

从技术上讲,编译内联代码 ${ inspectCode('x) }编译时调用方法 inspectCode(通过 Java 反射),然后方法 inspectCode 作为普通代码执行。

为了能够执行 inspectCode,我们需要先编译它的源代码。作为技术上的结果,我们不能在同一个类/文件中定义和使用宏。但是,只要可以先编译宏的实现,就可以在同一个项目中定义和调用宏。

挂起文件

为了允许在同一个项目中定义和使用宏,只扩展那些已经编译过的宏调用。对于所有其他(未知)宏调用,文件的编译将挂起。挂起文件只有在所有非挂起文件成功编译后才会编译。在某些情况下,您将遇到循环依赖,这将阻止编译完成。要获取有关哪些文件被挂起的更多信息,可以使用 -Xprint-suspension 编译器标志。

示例:使用宏静态评估 power

让我们回顾一下我们在关于 内联 的部分中定义的 power,它专门针对静态已知值的 n 计算 xⁿ

inline def power(x: Double, inline n: Int): Double =
  inline if n == 0 then 1.0
  else inline if n % 2 == 1 then x * power(x, n - 1)
  else power(x * x, n / 2)

在本节的剩余部分,我们将定义一个宏,它针对静态已知值 xn 计算 xⁿ。虽然这也可以完全使用 inline 来实现,但使用宏来实现它将说明一些事情。

inline def power(inline x: Double, inline n: Int) =
  ${ powerCode('x, 'n)  }

def powerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] = ...

简单表达式

我们可以如下实现 powerCode

def pow(x: Double, n: Int): Double =
  if n == 0 then 1 else x * pow(x, n - 1)

def powerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] =
  val value: Double = pow(x.valueOrAbort, n.valueOrAbort)
  Expr(value)

这里,pow 操作是一个简单的 Scala 函数,用于计算 xⁿ 的值。有趣的是我们如何创建和查看 Expr

从值创建表达式

首先让我们看一下 Expr.apply(value)。给定一个类型为 T 的值,此调用将返回一个包含表示给定值的代码的表达式(即类型为 Expr[T])。Expr 的参数值在编译时计算,在运行时我们只需要实例化此值。

从值创建表达式适用于所有基本类型、任意元组、ClassArraySeqSetListMapOptionEitherBigIntBigDecimalStringContext。如果为其他类型实现了 ToExpr,则也可以使用它们,我们将在稍后看到这一点

从表达式中提取值

我们在 powerCode 实现中使用的第二个方法是 Expr[T].valueOrAbort,它与 Expr.apply 的作用相反。它尝试从类型为 Expr[T] 的表达式中提取类型为 T 的值。只有当表达式直接包含值的代码时,这才能成功,否则,它将抛出一个异常,停止宏扩展并报告表达式不对应于值。

除了 valueOrAbort,我们还可以使用 value 操作,它将返回一个 Option。这样我们就可以用自定义错误消息报告错误。

报告自定义错误消息

上下文 Quotes 参数提供了一个 report 对象,我们可以用它来报告自定义错误消息。在宏实现方法中,您可以使用 quotes 方法(使用 import scala.quoted.* 导入)访问上下文 Quotes 参数,然后通过 import quotes.reflect.report 导入 report 对象。

提供自定义错误

我们可以通过在 report 对象上调用 errorAndAbort 来提供自定义错误消息,如下所示

def powerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] =
  import quotes.reflect.report
  (x.value, n.value) match
    case (Some(base), Some(exponent)) =>
      val value: Double = pow(base, exponent)
      Expr(value)
    case (Some(_), _) =>
      report.errorAndAbort("Expected a known value for the exponent, but was " + n.show, n)
    case _ =>
      report.errorAndAbort("Expected a known value for the base, but was " + x.show, x)

或者,我们也可以使用 Expr.unapply 提取器

  ...
  (x, n) match
    case (Expr(base), Expr(exponent)) =>
      val value: Double = pow(base, exponent)
      Expr(value)
    case (Expr(_), _) => ...
    case _ => ...

操作 valuevalueOrAbortExpr.unapply 将适用于所有原始类型、任意元数的元组OptionSeqSetMapEitherStringContext。如果为其他类型实现了 FromExpr,它们也可以工作,我们将在 稍后看到这一点

显示表达式

inspectCode 的实现中,我们已经看到了如何使用 .show 方法将表达式转换为其源代码的字符串表示形式。这对于对宏实现进行调试很有用

def debugPowerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] =
  println(
    s"powerCode \n" +
    s"  x := ${x.show}\n" +
    s"  n := ${n.show}")
  val code = powerCode(x, n)
  println(s"  code := ${code.show}")
  code

使用可变参数

Scala 中的可变参数用 Seq 表示,因此当我们编写一个带有可变参数的宏时,它将作为 Expr[Seq[T]] 传递。可以使用 scala.quoted.Varargs 提取器恢复每个单独的参数(类型为 Expr[T])。

import scala.quoted.* // imports `Varargs`, `Quotes`, etc.

inline def sumNow(inline nums: Int*): Int =
  ${ sumCode('nums)  }

def sumCode(nums: Expr[Seq[Int]])(using Quotes): Expr[Int] =
  import quotes.reflect.report
  nums match
    case  Varargs(numberExprs) => // numberExprs: Seq[Expr[Int]]
      val numbers: Seq[Int] = numberExprs.map(_.valueOrAbort)
      Expr(numbers.sum)
    case _ => report.errorAndAbort(
      "Expected explicit varargs sequence. " +
      "Notation `args*` is not supported.", nums)

提取器将匹配对 sumNow(1, 2, 3) 的调用,并提取一个包含每个参数代码的 Seq[Expr[Int]]。但是,如果我们尝试匹配调用 sumNow(nums*) 的参数,提取器将不匹配。

Varargs 也可以用作构造函数。 Varargs(Expr(1), Expr(2), Expr(3)) 将返回一个 Expr[Seq[Int]]。我们将在稍后看到这将如何有用。

复杂表达式

到目前为止,我们只看到了如何构造和解构对应于简单值的表达式。为了处理更复杂的表达式,Scala 3 提供了不同的元编程功能,包括

每个都增加了复杂性,并可能失去安全保证。通常建议优先使用简单的 API 而不是更高级的 API。在本节的剩余部分,我们将介绍一些额外的构造函数和析构函数,而后续章节将介绍更高级的 API。

集合

我们已经了解了如何使用 Expr.applyList[Int] 转换为 Expr[List[Int]]。如何将 List[Expr[Int]] 转换为 Expr[List[Int]]?我们提到 Varargs.apply 可以对序列执行此操作;同样,对于其他集合类型,也有相应的可用方法

  • Expr.ofList:将 List[Expr[T]] 转换为 Expr[List[T]]
  • Expr.ofSeq:将 Seq[Expr[T]] 转换为 Expr[Seq[T]](就像 Varargs 一样)
  • Expr.ofTupleFromSeq:将 Seq[Expr[T]] 转换为 Expr[Tuple]
  • Expr.ofTuple:将 (Expr[T1], ..., Expr[Tn]) 转换为 Expr[(T1, ..., Tn)]

简单块

构造函数 Expr.block 提供了一种简单的方法来创建代码块 { stat1; ...; statn; expr }。它的第一个参数是包含所有语句的列表,第二个参数是块末尾的表达式。

inline def test(inline ignore: Boolean, computation: => Unit): Boolean =
  ${ testCode('ignore, 'computation) }

def testCode(ignore: Expr[Boolean], computation: Expr[Unit])(using Quotes) =
  if ignore.valueOrAbort then Expr(false)
  else Expr.block(List(computation), Expr(true))

当我们想要生成包含多个副作用的代码时,Expr.block 构造函数很有用。宏调用 test(false, EXPRESSION) 将生成 { EXPRESSION; true},而调用 test(true, EXPRESSION) 将导致 false

简单匹配

方法 Expr.matches 可用于检查一个表达式是否等于另一个表达式。使用此方法,我们可以为 Expr[Boolean] 实现一个 value 操作,如下所示。

def value(boolExpr: Expr[Boolean]): Option[Boolean] =
  if boolExpr.matches(Expr(true)) then Some(true)
  else if boolExpr.matches(Expr(false)) then Some(false)
  else None

它也可以用于比较两个用户编写的表达式。请注意,matches 只执行有限数量的规范化,虽然例如 Scala 表达式 2 与表达式 { 2 } 匹配,但对于表达式 { val x: Int = 2; x } 则 _不匹配_。

任意表达式

最后但并非最不重要的一点是,可以通过将任意 Scala 代码包含在 引号 中来创建 Expr[T]。例如,'{ ${expr}; true } 将生成一个等效于 Expr.block(List(expr), Expr(true))Expr[Int]。关于 引号代码 的后续部分将更详细地介绍引号。

本页贡献者