揭秘 Scala 3 宏:深入探讨引用与拼接

发布: (2025年12月8日 GMT+8 02:48)
5 min read
原文: Dev.to

Source: Dev.to

引言

欢迎来到 Scala 3 编译时元编程的世界!宏看起来可能很神奇,但它们是建立在强大的 quoting(引用)和 splicing(拼接)系统之上的。本文我们将剖析一个实用宏,它能够把任意 case 类转换为 Map[String, Any]。阅读完后,你将了解宏的功能、工作原理以及驱动它的底层反射概念。

宏代码

package com.dev24.macros

import scala.quoted.*

object ToMapMacro {
  // The user‑facing, inline method
  extension [A](inline a: A) inline def toMap: Map[String, Any] = ${ toMapImpl('a) }

  // The implementation that runs at compile time
  def toMapImpl[A: Type](instance: Expr[A])(using Quotes): Expr[Map[String, Any]] = {
    import quotes.reflect.*

    // 1. Get the type and symbol of A
    val tpe = TypeRepr.of[A]
    val sym = tpe.typeSymbol

    // 2. Get the list of case class fields
    val fields: List[Symbol] =
      if (sym.isClassDef) sym.caseFields.toList
      else Nil

    // 3. For each field, generate a quoted (String, Any) tuple
    val mapEntries: List[Expr[(String, Any)]] = fields.map { field =>
      val fieldNameExpr = Expr(field.name)
      val fieldAccess = Select(instance.asTerm, field)
      val fieldValueExpr = fieldAccess.asExprOf[Any]
      '{ ($fieldNameExpr, $fieldValueExpr) }
    }

    // 4. Assemble the final Map from the list of tuples
    '{ Map(${Varargs(mapEntries)}*) }
  }
}

第 1 部分 – 元编程的入口:inline 方法

extension [A](inline a: A) inline def toMap: Map[String, Any] = ${ toMapImpl('a) }
  • extension [A] 为任意类型 A 添加方法,使得可以像 myCaseClass.toMap 这样调用。
  • inline a: A 告诉编译器在编译期间用宏的结果替换调用点,而不是在运行时执行普通方法。
  • ${ … }splice(拼接)运算符,用于把普通代码桥接到宏代码。内部的表达式由编译器执行。
  • toMapImpl('a) 调用宏实现,并传入 quoted(已引用)的 a(即 'a)。引用会把值提升为抽象语法树(AST,Expr[A]),而不是运行时的实际值。

当编译器看到 person.toMap 时,它会暂停编译,使用 person 的 quoted AST 调用 toMapImpl,得到一个新的 quoted Map,随后把这段代码拼回程序中。

第 2 部分 – 宏的核心实现

def toMapImpl[A: Type](instance: Expr[A])(using Quotes): Expr[Map[String, Any]]
  • A: Type 是上下文界定,提供 Type[A](编译时类型标签),使宏能够检查 A 的结构。
  • instance: Expr[A] 是被转换值的 quoted AST。
  • using Quotes 提供宏的工具箱,允许访问反射 API。
  • 方法返回的是 quotedMap[String, Any]Expr[Map[String, Any]]),而不是实际的 Map。

底层反射

import quotes.reflect.* 引入了核心反射类型:

  • TypeRepr – 类型的丰富内部表示。
  • Symbol – 任意声明(类、字段、方法等)的唯一标识符。
  • Term – 表达式或语句的 AST 节点。
  • Expr – 对 Term 的类型安全包装。

步骤 1 – 检查类型

val tpe = TypeRepr.of[A]
val sym = tpe.typeSymbol
  • TypeRepr.of[A] 获取 A 的完整编译时描述。
  • tpe.typeSymbol 提取该类型的主 Symbol(例如 case 类的类定义)。

步骤 2 – 找到字段

val fields: List[Symbol] =
  if (sym.isClassDef) sym.caseFields.toList
  else Nil
  • sym.isClassDef 确认该符号代表一个类。
  • sym.caseFields 返回 case 类的字段符号列表;否则返回空列表。

步骤 3 – 生成 Map 条目

val mapEntries: List[Expr[(String, Any)]] = fields.map { field =>
  val fieldNameExpr = Expr(field.name)
  val fieldAccess = Select(instance.asTerm, field)
  val fieldValueExpr = fieldAccess.asExprOf[Any]
  '{ ($fieldNameExpr, $fieldValueExpr) }
}
  • 对每个字段,我们创建一个 quoted 元组 (fieldName, fieldValue)
  • Select(instance.asTerm, field) 构造一个访问该实例字段的 term。
  • 使用 '{ … } 将元组提升为 quoted 表达式。

步骤 4 – 组装最终 Map

'{ Map(${Varargs(mapEntries)}*) }
  • Varargs(mapEntries) 把元组表达式列表作为可变参数拼接进 Map.apply
  • 整个表达式被 quoted,生成将在运行时构造 Map 的代码。

结论

宏的工作流程如下:

  1. 接收 case 类实例的 quoted 表示。
  2. 使用编译时反射发现该类的字段。
  3. 生成构建 Map[String, Any] 的 quoted 代码。
  4. 将生成的代码拼回调用点。

这展示了 Scala 3 的 quoting 与 splicing 如何结合底层反射,实现强大的编译时代码生成,同时保持面向用户的 API 简洁且惯用。

Back to Blog

相关文章

阅读更多 »

Scala的起源 (2009)

请提供您希望翻译的具体摘录或摘要文本,我才能为您进行简体中文翻译。