Scala 3 中的宏

引号代码

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

代码块

引号代码块 '{ ... } 在语法上类似于字符串引号 " ... ",不同之处在于前者包含类型化代码。要将代码插入到其他代码中,我们可以使用语法 $expr${ expr },其中 expr 的类型为 Expr[T]。直观地说,引号中的代码 ('{ ... }) 不会立即执行,而拼接中的代码 (${ ... }) 会被计算,并将结果拼接进周围的表达式中。

val msg = Expr("Hello")
val printHello = '{ print($msg) }
println(printHello.show) // print("Hello")

一般来说,引用会延迟执行,而拼接会在周围代码之前执行。这种概括允许我们为不在引用中的 ${ ... } 赋予含义。这会在编译时评估拼接中的代码,并将结果放置在生成的代码中。由于一些技术考虑,只有顶级拼接才允许直接在 inline 定义中,我们称之为

可以在引用中编写引用,但在编写宏时这种模式并不常见。

级别一致性

不能在引用和拼接中编写任意代码,因为程序的一部分将在编译时执行,而另一部分将在运行时执行。考虑以下构建不当的代码

def myBadCounter1(using Quotes): Expr[Int] = {
  var x = 0
  '{ x += 1; x }
}

此代码的问题在于 x 在编译期间存在,但我们尝试在编译器完成后(甚至在另一台机器上)使用它。显然,不可能访问其值并对其进行更新。

现在考虑双重版本,我们在运行时定义变量并尝试在编译时访问它

def myBadCounter2(using Quotes): Expr[Int] = '{
  var x = 0
  ${ x += 1; 'x }
}

显然,这不起作用,因为变量还不存在。

为了确保无法编写包含此类问题的程序,我们限制了引用环境中允许的引用类型。

我们引入级别作为引用数减去表达式或定义周围的拼接数。

// level 0
'{ // level 1
  var x = 0
  ${ // level 0
    x += 1
    'x // level 1
  }
}

系统将允许在任何级别引用全局定义,例如 println,但会限制对局部定义的引用。只有当局部定义在与其引用相同的级别定义时,才能访问它。这将捕获 myBadCounter1myBadCounter2 中的错误。

即使无法在引用中引用变量,我们仍然可以通过使用 Expr.apply 将其当前值提升到表达式来传递引用。

泛型

在对带引号的代码使用类型参数或其他类型的抽象类型时,我们需要显式跟踪其中一些类型。Scala 为其泛型使用擦除类型语义。这意味着在编译时从程序中删除类型,并且运行时不必在运行时跟踪所有类型。

考虑以下代码

def evalAndUse[T](x: Expr[T])(using Quotes) = '{
  val x2: T = $x // error
  ... // use x2
}

在这里,我们将收到一条错误消息,告诉我们缺少上下文 Type[T]。因此,我们可以通过编写以下内容轻松修复它

def evalAndUse[T](x: Expr[T])(using Type[T])(using Quotes) = '{
  val x2: T = $x
  ... // use x2
}

此代码等同于此更详细的版本

def evalAndUse[T](x: Expr[T])(using t: Type[T])(using Quotes) = '{
  val x2: t.Underlying = $x
  ... // use x2
}

请注意,Type 具有一个名为 Underlying 的类型成员,它引用 Type 中包含的类型;在此情况下,t.UnderlyingT。即使我们隐式使用 Type,通常最好将其保持在上下文中,因为引号内的某些更改可能需要它。较不详细的版本通常是编写类型的最佳方式,因为它更易于阅读。在某些情况下,我们不会静态地知道 Type 中的类型,并且需要使用 t.Underlying 来引用它。

我们何时需要此额外的 Type 参数?

  • 当一个类型是抽象的,并且它在高于当前级别的级别上使用时。

当您向方法添加 Type 上下文参数时,您将从另一个上下文参数中获取它,或者隐式地调用 Type.of

evalAndUse(Expr(3))
// is equivalent to
evalAndUse[Int](Expr(3))(using Type.of[Int])

正如您可能猜到的那样,并非每种类型都可以直接用作 Type.of[..] 的参数。例如,我们无法恢复已擦除的抽象类型

def evalAndUse[T](x: Expr[T])(using Quotes) =
  given Type[T] = Type.of[T] // error
  '{
    val x2: T = $x
    ... // use x2
  }

但我们可以编写依赖于这些抽象类型的更复杂的类型。例如,如果我们查找或显式构造 Type[List[T]],则系统将在当前上下文中需要 Type[T] 进行编译。

良好的代码应该只将 Type 添加到上下文参数中,而绝不显式使用它们。但是,显式使用在调试时很有用,尽管它以简洁性和清晰性为代价。

ToExpr

Expr.apply 方法使用 ToExpr 的实例生成一个表达式,该表达式将创建该值的副本。

object Expr:
  def apply[T](x: T)(using Quotes, ToExpr[T]): Expr[T] =
    summon[ToExpr[T]].apply(x)

ToExpr 定义如下

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

ToExpr.apply 方法将获取一个值 T 并生成将在运行时构造此值的副本的代码。

我们可以定义我们自己的 ToExpr,如下所示

given ToExpr[Boolean] with {
  def apply(x: Boolean)(using Quotes) =
    if x then '{true}
    else '{false}
}

given ToExpr[StringContext] with {
  def apply(stringContext: StringContext)(using Quotes) =
    val parts = Varargs(stringContext.parts.map(Expr(_)))
    '{ StringContext($parts*) }
}

Varargs 构造函数只创建一个 Expr[Seq[T]],我们可以有效地将其作为 varargs 进行拼接。一般来说,任何序列都可以用 $mySeq* 进行拼接,以将其作为 varargs 进行拼接。

带引号的模式

引号还可以用来检查一个表达式是否等于另一个表达式,或者将一个表达式分解成各个部分。

匹配确切的表达式

我们可以做的最简单的事情是检查一个表达式是否与另一个已知的表达式匹配。下面,我们展示了如何使用 case '{...} => 匹配一些表达式。

def valueOfBoolean(x: Expr[Boolean])(using Quotes): Option[Boolean] =
  x match
    case '{ true } => Some(true)
    case '{ false } => Some(false)
    case _ => None

def valueOfBooleanOption(x: Expr[Option[Boolean]])(using Quotes): Option[Option[Boolean]] =
  x match
    case '{ Some(true) } => Some(Some(true))
    case '{ Some(false) } => Some(Some(false))
    case '{ None } => Some(None)
    case _ => None

匹配部分表达式

为了使内容更紧凑,我们还可以使用拼接 ($) 来匹配任意代码并提取它,从而匹配表达式的部分。

def valueOfBooleanOption(x: Expr[Option[Boolean]])(using Quotes): Option[Option[Boolean]] =
  x match
    case '{ Some($boolExpr) } => Some(valueOfBoolean(boolExpr))
    case '{ None } => Some(None)
    case _ => None

匹配表达式的类型

我们还可以与任意类型 T 的代码进行匹配。下面,我们与类型为 T$x 进行匹配,并得到一个类型为 Expr[T]x

def exprOfOption[T: Type](x: Expr[Option[T]])(using Quotes): Option[Expr[T]] =
  x match
    case '{ Some($x) } => Some(x) // x: Expr[T]
    case '{ None } => Some(None)
    case _ => None

我们还可以检查表达式的类型

def valueOf(x: Expr[Any])(using Quotes): Option[Any] =
  x match
    case '{ $x: Boolean } => valueOfBoolean(x) // x: Expr[Boolean]
    case '{ $x: Option[Boolean] }  => valueOfBooleanOption(x) // x: Expr[Option[Boolean]]
    case _ => None

或者类似地检查部分表达式的类型

case '{ Some($x: Boolean) } => // x: Expr[Boolean]

匹配方法的接收者

当我们想要匹配一个方法的接收者时,我们需要明确地声明它的类型

case '{ ($ls: List[Int]).sum } =>

如果我们写 $ls.sum,我们将无法知道 ls 的类型以及我们正在调用哪个 sum 方法。

我们需要类型注释的另一个常见情况是中缀运算

case '{ ($x: Int) + ($y: Int) } =>
case '{ ($x: Double) + ($y: Double) } =>
case ...

匹配函数表达式

即将推出

匹配类型

到目前为止,我们假设引号模式中的类型是静态已知的。引号模式还允许泛型类型和存在类型,我们将在本节中看到。

模式中的泛型类型

考虑我们已经看到过的函数 exprOfOption

def exprOfOption[T: Type](x: Expr[Option[T]])(using Quotes): Option[Expr[T]] =
  x match
    case '{ Some($x: T) } => Some(x) // x: Expr[T]
                // ^^^ type ascription with generic type T
    ...

请注意,这次我们在模式中显式添加了 T,即使可以推断出来。通过在模式中引用泛型类型 T,我们要求在范围内有一个给定的 Type[T]。这意味着 $x: T 仅在 xExpr[T] 类型时匹配。在这种特殊情况下,此条件始终为真。

现在考虑以下变体,其中 x 是具有(静态)未知元素类型的可选值

def exprOfOptionOf[T: Type](x: Expr[Option[Any]])(using Quotes): Option[Expr[T]] =
  x match
    case '{ Some($x: T) } => Some(x) // x: Expr[T]
    case _ => None

这次,模式 Some($x: T) 仅在 Option 的类型为 Some[T] 时匹配。

exprOfOptionOf[Int]('{ Some(3) })   // Some('{3})
exprOfOptionOf[Int]('{ Some("a") }) // None

引号模式中的类型变量

引号代码可能包含引号外部未知的类型。我们可以使用模式类型变量匹配它们。与普通模式一样,类型变量使用小写名称编写。

def exprOptionToList(x: Expr[Option[Any]])(using Quotes): Option[Expr[List[Any]]] =
  x match
    case '{ Some($x: t) } =>
                // ^^^ this binds the type `t` in the body of the case
      Some('{ List[t]($x) }) // x: Expr[List[t]]
    case '{ None } =>
      Some('{ Nil })
    case _ => None

模式 $x: t 将匹配任何类型的表达式,并且 t 将绑定到模式的类型。此类型变量仅在 case 的右侧有效。在此示例中,我们使用它来构造列表 List[t]($x)List($x) 也适用)。由于这是一个静态未知的类型,因此我们需要在范围内给定 Type[t]。幸运的是,引号模式会自动为我们提供此信息。

如果我们想了解表达式的确切类型,那么简单的模式 case '{ $expr: tpe } => 非常有用。

val expr: Expr[Option[Int]] = ...
expr match
  case '{ $expr: tpe } =>
    Type.show[tpe] // could be: Option[Int], Some[Int], None, Option[1], Option[2], ...
    '{ val x: tpe = $expr; x } // binds the value without widening the type
    ...

在某些情况下,我们需要定义一个模式变量,它被多次引用或具有一些类型边界。要实现此目的,可以使用 type t 连同类型模式变量在模式的开头创建模式变量。

/**
 * Use: Converts a redundant `list.map(f).map(g)` to only use one call
 * to `map`: `list.map(y => g(f(y)))`.
 */
def fuseMap[T: Type](x: Expr[List[T]])(using Quotes): Expr[List[T]] = x match {
  case '{
    type u
    type v
    ($ls: List[`u`])
      .map($f: `u` => `v`)
      .map($g: `v` => T)
    } =>
    '{ $ls.map(y => $g($f(y))) }
  case _ => x
}

在此,我们定义两个类型变量 uv,然后使用 `u``v` 引用它们。我们不使用 uv(不带反引号)引用它们,因为这些引用会被解释为具有相同变量名称的新类型变量。此表示法遵循正常的 稳定标识符模式 语法。此外,如果需要约束类型变量,我们可以在类型定义中直接添加边界:case '{ type u <: AnyRef; ... } =>

请注意,也可以将前一个案例写成 case '{ ($ls: List[u]).map[v]($f).map[T]($g) =>

引用类型模式

使用 Type[T] 表示的类型可以使用模式 case '[...] => 匹配。

inline def mirrorFields[T]: List[String] = ${mirrorFieldsImpl[T]}

def mirrorFieldsImpl[T: Type](using Quotes): Expr[List[String]] =

  def rec[A : Type]: List[String] = Type.of[A] match
    case '[field *: fields] =>
      Type.show[field] :: rec[fields]
    case '[EmptyTuple] =>
      Nil
    case _ =>
      quotes.reflect.report.errorAndAbort("Expected known tuple but got: " + Type.show[A])

  Expr(rec)
mirrorFields[EmptyTuple]         // Nil
mirrorFields[(Int, String, Int)] // List("scala.Int", "java.lang.String", "scala.Int")
mirrorFields[Tuple]              // error: Expected known tuple but got: Tuple

与表达式引用模式一样,类型变量使用小写名称表示。

FromExpr

Expr.valueExpr.valueOrAbortExpr.unapply 方法使用 FromExpr 的实例在可能的情况下提取值。

extension [T](expr: Expr[T]):
  def value(using Quotes)(using fromExpr: FromExpr[T]): Option[T] =
    fromExpr.unapply(expr)

  def valueOrError(using Quotes)(using fromExpr: FromExpr[T]): T =
    fromExpr.unapply(expr).getOrElse(eport.throwError("...", expr))
end extension

object Expr:
  def unapply[T](expr: Expr[T])(using Quotes)(using fromExpr: FromExpr[T]): Option[T] =
    fromExpr.unapply(expr)

FromExpr 定义如下

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

FromExpr.unapply 方法将获取一个值 x 并生成将在运行时构造此值副本的代码。

我们可以像这样定义我们自己的 FromExpr

given FromExpr[Boolean] with {
  def unapply(x: Expr[Boolean])(using Quotes): Option[Boolean] =
    x match
      case '{ true } => Some(true)
      case '{ false } => Some(false)
      case _ => None
}

given FromExpr[StringContext] with {
  def unapply(x: Expr[StringContext])(using Quotes): Option[StringContext] = x match {
    case '{ new StringContext(${Varargs(Exprs(args))}*) } => Some(StringContext(args*))
    case '{     StringContext(${Varargs(Exprs(args))}*) } => Some(StringContext(args*))
    case _ => None
  }
}

请注意,我们处理了 StringContext 的两种情况。由于它是一个 case class,因此可以使用 new StringContext 或伴随对象中的 StringContext.apply 创建它。我们还使用了 Varargs 提取器将类型为 Expr[Seq[String]] 的参数匹配到 Seq[Expr[String]] 中。然后,我们使用 Exprs 匹配 Seq[Expr[String]] 中已知的常量以获取 Seq[String]

引号

Quotes 是创建所有引号的主要入口点。此上下文通常仅通过上下文抽象(using?=>)传递。每个引号范围都将有其自己的 Quotes。每次引入拼接时都会引入新的范围(${ ... })。虽然看起来拼接将表达式作为参数,但它实际上采用 Quotes ?=> Expr[T]。因此,我们实际上可以明确地将其写为 ${ (using q) => ... }。在调试时,这可能有助于避免为这些范围生成名称。

方法 scala.quoted.quotes 提供了一种简单的方法来使用当前 Quotes,而无需对其命名。它通常与 Quotes 一起导入,使用 import scala.quoted.*

${ (using q1) => body(using q1) }
// equivalent to
${ body(using quotes) }

警告:如果您显式命名 Quotes quotes,您将隐藏此定义。

当我们在宏中编写顶级拼接时,我们正在调用类似于以下定义的内容。此拼接将提供与宏扩展关联的初始 Quotes

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

当我们在引号中进行拼接时,内部引号上下文将取决于外部引号上下文。此链接使用 Quotes.Nested 类型表示。引号的用户几乎不需要使用 Quotes.Nested。这些详细信息仅对将检查代码并可能遇到引号和拼接的详细信息的高级宏有用。

def f(using q1: Quotes) = '{
  ${ (using q2: q1.Nested) ?=>
      ...
  }
}

我们可以想象一个嵌套拼接就像以下方法,其中 ctx 是周围引号接收的上下文。

def $[T](using q: Quotes)(x: q.Nested ?=> Expr[T]): T = ...

β-归约

当我们在引号 '{ ((x: Int) => x + x)(y) } 中将 lambda 应用于参数时,我们不会在引号中对其进行归约;代码保持原样。有一个优化,它将直接应用于参数的所有 lambda 进行 β-归约,以避免创建闭包。这将不会从引号的角度可见。

有时直接在引号上执行此 β-归约很有用。我们提供了函数 Expr.betaReduce[T],它接收一个 Expr[T],并在包含直接应用的 lambda 时进行 β-归约。

Expr.betaReduce('{ ((x: Int) => x + x)(y) }) // returns '{ val x = y; x + x }

召唤值

在宏中召唤值有两种方法。第一种是在内联方法中有一个 using 参数,该参数显式传递给宏实现。

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

def setOfCode[T: Type](ord: Expr[Ordering[T]])(using Quotes): Expr[Set[T]] =
  '{ TreeSet.empty[T](using $ord) }

在此场景中,在宏展开之前找到上下文参数。如果未找到,则不会展开宏。

第二种方法是使用 Expr.summon。这允许我们以编程方式搜索不同的给定表达式。以下示例类似于前面的示例

inline def setOf[T]: Set[T] =
  ${ setOfCode[T] }

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

不同之处在于,在第二种场景中,我们在执行隐式搜索之前展开宏。因此,我们可以编写任意代码来处理未找到 Ordering[T] 的情况。在此,我们使用 HashSet 而不是 TreeSet,因为前者不需要 Ordering

引用类型类

在前面的示例中,我们展示了如何通过利用 using 参数子句显式使用 Expr[Ordering[T]] 类型类。这完全没问题,但如果我们需要多次使用类型类,则不是很方便。为了说明这一点,我们将使用一个 powerCode 函数,该函数可用于任何数字类型。

首先,使 Expr 类型类成为给定参数可能很有用。为此,我们确实需要在 power 中显式地转换为 powerCode,因为我们有一个给定的 Numeric[Num],但需要一个 Expr[Numeric[Num]]。但随后我们可以忽略它在 powerMacro 和任何其他仅传递它的位置。

inline def power[Num](x: Num, inline n: Int)(using num: Numeric[Num]) =
  ${ powerMacro('x, 'n)(using 'num) }

def powerMacro[Num: Type](x: Expr[Num], n: Expr[Int])(using Expr[Numeric[Num]])(using Quotes): Expr[Num] =
  powerCode(x, n.valueOrAbort)

要使用此类型类,我们需要一个给定的 Numeric[Num],但我们有一个 Expr[Numeric[Num]],因此我们需要在生成的代码中拼接此表达式。为了使其可用,我们只需在给定定义中对其进行拼接。

def powerCode[Num: Type](x: Expr[Num], n: Int)(using num: Expr[Numeric[Num]])(using Quotes): Expr[Num] =
  if (n == 0) '{ $num.one }
  else if (n % 2 == 0) '{
    given Numeric[Num] = $num
    val y = $x * $x
    ${ powerCode('y, n / 2) }
  }
  else '{
    given Numeric[Num] = $num
    $x * ${ powerCode(x, n - 1) }
  }

此页面的贡献者