类型提供程序

语言
此文档页面特定于 Scala 2 中提供的功能,这些功能已在 Scala 3 中删除或被替代功能取代。除非另有说明,本页面中的所有代码示例均假定你使用的是 Scala 2。

实验性

尤金·布尔马科

类型提供程序并非作为专用宏风格实现,而是建立在 Scala 宏已提供的功能之上。

模拟类型提供程序有两种策略:一种基于结构化类型(称为“匿名类型提供程序”),另一种基于宏注释(称为“公共类型提供程序”)。前者建立在 2.10.x、2.11.x 和 2.12.x 中可用的功能之上,而后者需要宏天堂。这两种策略都可以用来实现如下所述的擦除类型提供程序。

请注意,宏天堂在编译和扩展宏注释时都是必需的,这意味着公共类型提供程序的作者和用户都必须将宏天堂添加到他们的构建中。但是,在宏注释扩展后,生成代码将不再引用宏天堂,并且在编译时或运行时都不需要它。

最近我们发表了一篇关于 Scala 中基于宏的类型提供程序的演讲,总结了最先进的技术并提供了具体示例。幻灯片和随附代码可在 https://github.com/travisbrown/type-provider-examples 找到。

引言

类型提供程序是一种强类型类型桥接机制,它支持 F# 3.0 中的信息丰富编程。类型提供程序是一种编译时工具,它能够根据描述数据源的静态参数生成定义及其实现。类型提供程序可以在两种模式下运行:非擦除和擦除。前者类似于文本代码生成,因为每个生成的类型都变成字节码,而后一种情况下,生成的类型只在类型检查期间显示,但在字节码生成之前被擦除为程序员提供的上限。

在 Scala 中,宏扩展可以生成程序员喜欢的任何代码,包括 ClassDefModuleDefDefDef 和其他定义节点,因此类型提供程序的代码生成部分已涵盖。牢记这一点,为了模拟类型提供程序,我们需要解决另外两个挑战

  1. 使生成的定义公开可见(def 宏,Scala 2.10、2.11 和 2.12 中唯一可用的宏风格,在意义上是本地的,因为其扩展的范围有限:https://groups.google.com/d/msg/scala-user/97ARwwoaq2U/kIGWeiqSGzcJ)。
  2. 使生成的定义可选择擦除(Scala 支持对多种语言结构进行擦除,例如对于抽象类型成员和值类,但该机制不可扩展,这意味着宏编写器无法对其进行自定义)。

匿名类型提供程序

即使 def 宏扩展引入的定义的范围仅限于这些扩展,但这些定义可以通过转换为结构类型来跳出其范围。例如,考虑 h2db 宏,它采用连接字符串并生成一个封装给定数据库的模块,扩展如下。

def h2db(connString: String): Any = macro ...

// an invocation of the `h2db` macro
val db = h2db("jdbc:h2:coffees.h2.db")

// expands into the following code
val db = {
  trait Db {
    case class Coffee(...)
    val Coffees: Table[Coffee] = ...
  }
  new Db {}
}

确实,宏扩展块之外的任何人无法直接引用 Coffee 类,但是如果我们检查 db 的类型,我们会发现一些有趣的东西。

scala> val db = h2db("jdbc:h2:coffees.h2.db")
db: AnyRef {
  type Coffee { val name: String; val price: Int; ... }
  val Coffees: Table[this.Coffee]
} = $anon$1...

正如我们所见,当类型检查器尝试为 db 推断类型时,它采用了对本地声明的类的所有引用,并将它们替换为包含这些类的所有公开可见成员的结构类型。结果类型捕获了生成类的本质,为其成员提供了静态类型化接口。

scala> db.Coffees.all
res1: List[Db$1.this.Coffee] = List(Coffee(Brazilian,99,0))

这种类型提供程序的方法非常简洁,因为它可以与 Scala 的生产版本一起使用,但它存在性能问题,原因是 Scala 在编译对结构类型成员的访问时会发出反射调用。有几种应对策略,但此页边距太窄,无法容纳它们,因此我为您推荐 Travis Brown 的精彩博客系列,以了解详情:文章 1文章 2文章 3

公共类型提供程序

借助 宏天堂 及其 宏注解,可以轻松生成公开可见的类,而无需应用基于结构类型的解决方法。基于注解的解决方案非常简单,因此我不会在这里写很多关于它的内容。

class H2Db(connString: String) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro ...
}

@H2Db("jdbc:h2:coffees.h2.db") object Db
println(Db.Coffees.all)
Db.Coffees.insert("Brazilian", 99, 0)

解决擦除问题

我们尚未对此进行详细研究,但有一个假设,即类型成员和单例类型的组合可以提供相当于 F# 中已擦除类型提供程序的内容。具体而言,我们不希望擦除的类应按通常方式声明,而应擦除到给定上限的类应声明为该上限的类型别名,该上限由携带唯一标识符的单例类型参数化。通过这种方法,每个新生成的类型仍会给类型别名的元数据带来额外的字节码开销,但该字节码将明显小于成熟类的字节码。此技术适用于匿名和公共类型提供程序。

object Netflix {
  type Title = XmlEntity["https://.../Title".type]
  def Titles: List[Title] = ...
  type Director = XmlEntity["https://.../Director".type]
  def Directors: List[Director] = ...
  ...
}

class XmlEntity[Url] extends Dynamic {
  def selectDynamic(field: String) = macro XmlEntity.impl
}

object XmlEntity {
  def impl(c: Context)(field: c.Tree) = {
    import c.universe._
    val TypeRef(_, _, tUrl) = c.prefix.tpe
    val ConstantType(Constant(sUrl: String)) = tUrl
    val schema = loadSchema(sUrl)
    val Literal(Constant(sField: String)) = field
    if (schema.contains(sField)) q"${c.prefix}($sField)"
    else c.abort(s"value $sField is not a member of $sUrl")
  }
}

黑盒与白盒

匿名和公共类型提供程序都必须是 白盒。如果您将类型提供程序宏声明为 黑盒,它将不起作用。

此页面的贡献者