隐式宏

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

实验性

Eugene Burmako

隐式宏自 2.10.0 版本以来作为 Scala 的一项实验性功能提供,包括即将推出的 2.11.0,但需要 2.10.2 中的关键错误修复才能完全投入使用。隐式宏不需要宏天堂才能工作,无论是在 2.10.x 中还是在 2.11 中。

隐式宏的扩展,称为 fundep 实体化,在 2.10.0 到 2.10.4 中不可用,但在 宏天堂、Scala 2.10.5 和 Scala 2.11.x 中已实现。请注意,在 2.10.0 到 2.10.4 中,fundep 实体化宏的扩展需要宏天堂,这意味着您的用户必须将其构建添加到他们的构建中才能使用您的 fundep 实体化。但是,在 fundep 实体化扩展之后,结果代码将不再引用宏天堂,并且在编译时或运行时不需要它的存在。还要注意,在 2.10.5 中,fundep 实体化宏的扩展可以在没有宏天堂的情况下发生,但您的用户必须启用 -Yfundep-materialization 编译器标志。

隐式宏

类型类

下面的示例定义了 Showable 类型类,它抽象了 prettyprinting 策略。随附的 show 方法采用两个参数:一个显式参数(目标)和一个隐式参数(承载 Showable 的实例)。

trait Showable[T] { def show(x: T): String }
def show[T](x: T)(implicit s: Showable[T]) = s.show(x)

在像这样声明之后,可以仅提供目标来调用 show,并且 scalac 将尝试根据目标的类型从调用站点的范围推断相应的类型类实例。如果范围内有匹配的隐式值,则将推断该值并且编译将成功,否则将发生编译错误。

implicit object IntShowable extends Showable[Int] {
  def show(x: Int) = x.toString
}
show(42) // "42"
show("42") // compilation error

样板文本的扩散

类型类的一个众所周知的问题,总体而言,特别是在 Scala 中,是类似类型的实例定义通常非常相似,这会导致样板代码的扩散。

例如,对于许多对象,prettyprinting 意味着打印其类的名称以及字段的名称和值。即使这种和类似的配方非常简洁,但在实践中通常不可能简洁地实现它们,因此程序员被迫一遍又一遍地重复自己。

class C(x: Int)
implicit def cShowable = new Showable[C] {
  def show(c: C) = "C(" + c.x + ")"
}

class D(x: Int)
implicit def dShowable = new Showable[D] {
  def show(d: D) = "D(" + d.x + ")"
}

这个用例可以用运行时反射来实现,但反射通常要么因为擦除而太不精确,要么因为强加的开销而太慢。

还存在基于类型级别编程的泛型编程方法,例如,Lars Hupel 引入的 TypeClass 类型类技术,但与手动编写的类型类实例相比,它们也会受到性能影响。

隐式具体化器

使用隐式宏,可以完全消除手动定义类型类实例的样板代码,而无需牺牲性能。

trait Showable[T] { def show(x: T): String }
object Showable {
  implicit def materializeShowable[T]: Showable[T] = macro ...
}

程序员无需编写多个实例定义,而是可以在 Showable 类型类的伴生对象中定义一个 materializeShowable 宏。伴生对象的成员属于关联类型类的隐式范围,这意味着当程序员未提供 Showable 的显式实例时,将调用具体化器。调用后,具体化器可以获取 T 的表示形式并生成 Showable 类型类的适当实例。

隐式宏的一大优点是它们可以无缝地融入现有的隐式搜索基础设施。Scala 隐式的标准功能(例如多参数性和重叠实例)可供隐式宏使用,而无需程序员付出任何特殊努力。例如,可以为漂亮可打印元素的列表定义一个非宏美化打印器,并将其与基于宏的具体化器透明地集成。

implicit def listShowable[T](implicit s: Showable[T]) =
  new Showable[List[T]] {
    def show(x: List[T]) = { x.map(s.show).mkString("List(", ", ", ")")
  }
}
show(List(42)) // prints: List(42)

在这种情况下,所需的实例 Showable[Int] 将由上面定义的具体化宏生成。因此,通过使宏成为隐式的,它们可用于自动化类型类实例的具体化,同时与非宏隐式无缝集成。

Fundep 具体化

问题陈述

产生 fundep 具体化器的用例是由 Miles Sabin 和他的 shapeless 库提供的。在 shapeless 的旧版本(2.0.0 之前),Miles 定义了 Iso 特征,它表示类型之间的同构。 Iso 可用于将 case 类映射到元组,反之亦然(实际上,shapeless 使用 Iso 在 case 类和 HList 之间进行转换,但为简单起见,我们使用元组)。

trait Iso[T, U] {
  def to(t: T) : U
  def from(u: U) : T
}

case class Foo(i: Int, s: String, b: Boolean)
def conv[C, L](c: C)(implicit iso: Iso[C, L]): L = iso.to(c)

val tp  = conv(Foo(23, "foo", true))
tp: (Int, String, Boolean)
tp == (23, "foo", true)

如果我们尝试为 Iso 编写隐式具体化器,我们会遇到障碍。在类型检查 conv 等方法的应用时,scalac 必须推断类型参数 L,但它对此一无所知(这不足为奇,因为这是特定于领域的知识)。因此,当我们定义一个隐式宏来合成 Iso[C, L] 时,scalac 会在展开宏之前将 L 推断为 Nothing,然后一切都会崩溃。

建议的解决方案

正如 https://github.com/scala/scala/pull/2499 所示,针对概述问题的解决方案极其简单且优雅。

在 2.10 中,我们不允许宏应用程序展开,直到推断出它们的所有类型参数。但是,我们不必这样做。类型检查器可以尽可能多地进行推断(例如,在运行示例 C 中,将推断为 Foo,而 L 将保持未推断),然后停止。之后,我们展开宏,然后使用扩展的类型继续进行类型推断,以帮助类型检查器解决之前未确定的类型参数。这就是在 Scala 2.11.0 中实现它的方式。

可以在我们的 files/run/t5923c 测试中找到此技术的实际应用说明。请注意一切多么简单。materializeIso 隐式宏只获取其第一个类型参数并使用它来生成扩展。我们不需要理解第二个类型参数(尚未推断),我们不需要与类型推断进行交互 - 一切都会自动发生。

请注意,有一个关于 Nothing 的有趣警告,我们计划稍后解决它。

黑盒与白盒

香草具象化器(在本文件的第一个部分中介绍)既可以是 黑盒,也可以是 白盒

黑盒具象化器和白盒具象化器之间存在明显的区别。黑盒隐式宏扩展中的错误(例如显式的 c.abort 调用或扩展类型检查错误)将产生编译错误。白盒隐式宏扩展中的错误只会从当前隐式搜索中的隐式候选列表中删除宏,而不会向用户报告实际错误。这会产生权衡:黑盒隐式宏具有更好的错误报告功能,而白盒隐式宏更灵活,能够在必要时动态关闭自身。

Fundep 具象化器必须是白盒。如果你将 fundep 具象化器声明为黑盒,它将不起作用。

此页面的贡献者