Scala 3 能否用 Quotes 静态生成代码和元数据?
结论
可以用 Scala 3 Quotes 在宏展开时生成对应的 Form 代码。不过,Expr[Form] 表示一段待插入程序的表达式,并不是编译期已经构造好的 Form 实例。Quotes 由宏展开上下文提供,因此 createForm 可以作为宏实现或辅助函数使用,不能像普通方法一样在任意位置调用。
Scala 3 文档将 quote 描述为延后执行的代码,将 splice 描述为编译期执行并把结果插入外层代码的操作。宏通过 inline 定义中的顶层 splice 进入这个阶段。Scala 3 Quoted Code 文档 Scala 3 Macros 文档
用宏生成运行时代码
可以把 createForm 写成宏实现的辅助函数,再通过公开的 inline 方法触发宏:
import scala.quoted.*
case class Form(label: String)
inline def form: Form = ${ formImpl }
def formImpl(using Quotes): Expr[Form] =
'{ Form("Hello") }
调用 form 时,宏会在编译期展开,并将生成的 Form("Hello") 表达式插入调用方程序。生成后的程序在运行时执行,并创建 Form 实例。Expr[Form] 表示生成代码的结果类型是 Form,并不表示宏运行时已经拿到一个可序列化或可传给 renderFormOnClient 的实例。
原先的辅助函数也可以保留 Quotes 参数:
def createForm()(using Quotes): Expr[Form] =
'{ Form("Hello") }
inline def form: Form = ${ createForm() }
${ ... } 中调用的 createForm() 才是宏实现的一部分,此时编译器会提供 Quotes。如果要在运行时获取实例,应调用公开的 form,而不是直接调用返回 Expr[Form] 的辅助函数。
编译期写元数据文件
Quotes 主要用于构造、检查和转换程序表达式,并不是通用的构建文件生成接口。宏实现代码虽然可以执行普通 Scala 操作,但在宏展开时写文件容易与增量编译、并行编译和构建缓存发生协调问题。编译器文档也没有把这类副作用定义为可靠的元数据生成流程。
如果元数据需要成为构建产物,通常可以用 sbt 等构建工具的 source generator 或独立构建任务生成文件。运行时代码和生成任务共用同一份数据定义,可以避免分别维护两份结构。例如:
case class Form(label: String)
object Forms:
val hello = Form("Hello")
运行时代码可以直接使用 Forms.hello;构建任务则读取同一份声明或配置,将其序列化为 XML、JSON 等文件。构建任务如何发现这份定义,取决于项目使用的构建工具和数据组织方式。
如果结构只存在于宏展开时,也可以让宏生成描述结构的字符串或常量,再由构建流程收集并写入文件。不过,宏展开本身不保证文件会稳定写入指定位置。
选择方式
- 需要在运行时代码中得到
Form:让宏返回Expr[Form],并通过inline方法调用。 - 需要在编译产物中生成 XML 或 JSON 文件:由构建任务或 source generator 写入文件。
- 需要运行时和构建任务共用结构:将结构保存在普通数据或配置中,再分别生成运行时值和元数据。
Expr.value 等 API 只适用于表达式确实表示可提取的常量值等情况,不能用来把任意 Expr[Form] 取成已经运行的对象。Scala 3 的宏 FAQ 也建议,如果所需内容可以用 Expr(...) 表达,就尽量在编译期直接计算;只有需要延后到运行时执行时才使用 quote。Scala 3 宏 FAQ
备注:内容仅供参考。
