揭秘 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。- 方法返回的是 quoted 的
Map[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 的代码。
结论
宏的工作流程如下:
- 接收 case 类实例的 quoted 表示。
- 使用编译时反射发现该类的字段。
- 生成构建
Map[String, Any]的 quoted 代码。 - 将生成的代码拼回调用点。
这展示了 Scala 3 的 quoting 与 splicing 如何结合底层反射,实现强大的编译时代码生成,同时保持面向用户的 API 简洁且惯用。