此文档页面特定于 Scala 2 中提供的功能,这些功能在 Scala 3 中已被移除或被替代方案取代。除非另有说明,否则此页面中的所有代码示例都假设您使用的是 Scala 2。
已过时
Eugene Burmako
类型宏以前在 “宏天堂” 的早期版本中可用,但在宏天堂 2.0 中不再受支持。访问 天堂 2.0 公告 以了解解释和建议的迁移策略。
直觉
就像 def 宏在编译器看到某些方法的调用时执行自定义函数一样,类型宏允许在使用某些类型时将编译器挂钩。下面的代码片段展示了 H2Db
宏的定义和用法,该宏生成表示数据库中表的 case 类以及简单的 CRUD 功能。
type H2Db(url: String) = macro impl
object Db extends H2Db("coffees")
val brazilian = Db.Coffees.insert("Brazilian", 99, 0)
Db.Coffees.update(brazilian.copy(price = 10))
println(Db.Coffees.all)
H2Db
类型宏的完整源代码在 GitHub 上提供,本指南涵盖了其最重要的方面。首先,宏通过在编译时连接到数据库来生成静态类型的数据库包装器(树生成在 反射概述 中解释)。然后,它使用 NEW c.introduceTopLevel
API 将生成的包装器插入编译器维护的顶级定义列表中。最后,宏返回一个 Apply
节点,它表示对生成的类的超级构造函数调用。 注意 类型宏应该扩展为 c.Tree
,与 def 宏不同,def 宏扩展为 c.Expr[T]
。这是因为 Expr
表示项,而类型宏扩展为类型。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val name = c.freshName(c.enclosingImpl.name).toTypeName
val clazz = ClassDef(..., Template(..., generateCode()))
c.introduceTopLevel(c.enclosingPackage.pid.toString, clazz)
val classRef = Select(c.enclosingPackage.pid, name)
Apply(classRef, List(Literal(Constant(c.eval(url)))))
}
object Db extends H2Db("coffees")
// equivalent to: object Db extends Db$1("coffees")
与生成一个合成类并扩展为对它的引用不同,类型宏可以通过返回一个 Template
树来转换它的宿主。在 scalac 中,类和对象定义在内部都被表示为 Template
树的薄包装器,因此通过扩展到模板,类型宏有可能重写受影响的类或对象的整个主体。您可以在 GitHub 上看到这种技术的完整示例。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val Template(_, _, existingCode) = c.enclosingTemplate
Template(..., existingCode ++ generateCode())
}
object Db extends H2Db("coffees")
// equivalent to: object Db {
// <existing code>
// <generated code>
// }
细节
类型宏代表了 def 宏和类型成员之间的混合体。一方面,它们像方法一样定义(例如,它们可以具有值参数、具有上下文界限的类型参数等)。另一方面,它们属于类型的命名空间,因此,它们只能在需要类型的地方使用(请参阅 GitHub 上的完整示例),它们只能覆盖类型或其他类型宏等。
特性 | Def 宏 | 类型宏 | 类型成员 |
---|---|---|---|
是否分为 def 和 impl | 是 | 是 | 否 |
可以有值参数 | 是 | 是 | 否 |
可以有类型参数 | 是 | 是 | 是 |
… 带有方差注解 | 否 | 否 | 是 |
… 带有上下文界限 | 是 | 是 | 否 |
可以重载 | 是 | 是 | 否 |
可以继承 | 是 | 是 | 是 |
可以覆盖和被覆盖 | 是 | 是 | 是 |
在 Scala 程序中,类型宏可以以五种可能的角色之一出现:类型角色、应用类型角色、父类型角色、新角色和注解角色。根据宏使用的角色,可以使用 NEW c.macroRole
API 检查,其允许的扩展列表是不同的。
角色 | 示例 | 类 | 非类? | 应用? | 模板? |
---|---|---|---|---|---|
类型 | def x: TM(2)(3) = ??? |
是 | 是 | 否 | 否 |
应用类型 | class C[T: TM(2)(3)] |
是 | 是 | 否 | 否 |
父类型 | class C extends TM(2)(3) new TM(2)(3){} |
是 | 否 | 是 | 是 |
新 | new TM(2)(3) |
是 | 否 | 是 | 否 |
注解 | @TM(2)(3) class C |
是 | 否 | 是 | 否 |
简而言之,类型宏的扩展用它返回的树替换类型宏的使用。要确定扩展是否有意义,请在脑海中将宏的一些用法替换为其扩展,并检查生成的程序是否正确。
例如,在 class C extends TM(2)(3)
中用作 TM(2)(3)
的类型宏可以扩展为 Apply(Ident(TypeName("B")), List(Literal(Constant(2))))
,因为这将导致 class C extends B(2)
。但是,如果 TM(2)(3)
用作 def x: TM(2)(3) = ???
中的类型,则相同的扩展将没有意义,因为 def x: B(2) = ???
(假设 B
本身不是类型宏;如果是,它将被递归扩展,并且扩展的结果将决定程序的有效性)。
技巧和窍门
生成类和对象
使用类型宏,您可能会越来越发现自己处于 reify
不适用的区域,如 StackOverflow 上所述。在这种情况下,请考虑使用 quasiquotes(宏天堂中的另一个实验性功能)作为手动树构建的替代方案。