我正在尝试学习如何使用新的 Scala 3 Quotes API 编写宏。在此 API 中,各种 AST 元素通常定义了两种方法来引入它们:
apply
和 copy
,其中 copy
采用非描述性 original: Tree
、name: String
和 TypeTree
,而 apply
则采用非描述性Symbol
似乎还包含一个名字和 Type
。
以
ValDef
为例:
在 QuotesImpl.scala 中
object ValDef extends ValDefModule:
def apply(symbol: Symbol, rhs: Option[Term]): ValDef =
xCheckMacroAssert(!symbol.flags.is(Flags.Method), "expected a symbol without `Method` flag set")
withDefaultPos(tpd.ValDef(symbol.asTerm, xCheckedMacroOwners(xCheckMacroValidExpr(rhs), symbol).getOrElse(tpd.EmptyTree)))
def copy(original: Tree)(name: String, tpt: TypeTree, rhs: Option[Term]): ValDef =
tpd.cpy.ValDef(original)(name.toTermName, tpt, xCheckedMacroOwners(xCheckMacroValidExpr(rhs), original.symbol).getOrElse(tpd.EmptyTree))
// tpd.
def ValDef(sym: TermSymbol, rhs: LazyTree = EmptyTree, inferred: Boolean = false)(using Context): ValDef =
ta.assignType(untpd.ValDef(sym.name, TypeTree(sym.info, inferred), rhs), sym)
// tpd.cpy.
def ValDef(tree: Tree)(name: TermName, tpt: Tree, rhs: LazyTree)(using Context): ValDef =
tree match {
case tree: ValDef if (name == tree.name) && (tpt eq tree.tpt) && (rhs eq tree.unforcedRhs) => tree
case _ => finalize(tree, untpd.ValDef(name, tpt, rhs)(sourceFile(tree)))
}
我什么时候应该使用其中一种而不是另一种?我认为
copy
方法似乎没有做太多事情;名称和类型仍然可以更改。将 copy
与新元素的现有父元素(不是 ValDef
)一起使用是否合适,或者仅当我想要更改已存在于元素中的 copy
时才使用 ValDef
树上的同一个地方?
Refined
是另一个有趣的案例,因为它甚至没有 apply
方法:
trait RefinedModule { this: Refined.type =>
def copy(original: Tree)(tpt: TypeTree, refinements: List[Definition]): Refined
那么答案相当简单:你从不使用
QuotesImpl
,因为这是一个内部实现细节,可以随时修改而不会发出任何警告。
Quotes
是唯一的公共API,它只有apply
和unapply
。
不久前,ZIO 开发人员很恼火,因为他们从宏中调用内部/私有 API,并且在新的 Scala 版本发布时代码崩溃了,因为有人进行了重构,改变了内部组织方式。然后有人抱怨 Scala 承诺提供出色的兼容性保证,但一个小更新却破坏了他们的代码。
它很容易忽略了没有向后/前向兼容性涵盖这样的情况
val publicAPI: PublicAPI
publicAPI.asInstanceOf[internal.totallyprivate.APIImpl].internalMethod
据我所知,自从 Scala 3 团队开始测试是否有人依赖他们的 private 实现以避免更多戏剧性事件以来,但这仍然是制作脆弱代码的好方法,在任何更新时都可能会崩溃。
我可以想到一些有效的案例,为什么任何人都需要这样做,但使用
copy
而不是 apply
绝对不是其中之一。