类型宏

语言
此文档页面特定于 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(宏天堂中的另一个实验性功能)作为手动树构建的替代方案。

此页面的贡献者