此文档页面专门针对 Scala 2 中提供的功能,这些功能在 Scala 3 中已被删除或被替代功能所取代。除非另有说明,本页面中的所有代码示例均假定你使用的是 Scala 2。
实验性
Eugene Burmako
本文档列出了 Scala 2.11.0 开发周期中反射和宏的所有主要更改。首先,我们提供了最重要的修复和新引入功能的摘要,然后,在本文档的后面,我们将解释这些更改将如何影响与 Scala 2.10.x 的兼容性,以及如何在 2.10.x 和 2.11.0 中使基于反射的代码正常工作。
准引号
准引号是 Scala 2.11.0 中反射和宏最令人印象深刻的升级。它们由 Denys Shabalin 实现,极大地简化了全球 Scala 元程序员的生活。访问 专门的文档页面 以了解有关准引号的更多信息。
新的宏功能
1) Fundep 具象化。自 Scala 2.10.2 起,可以将隐式白盒宏用于具象化类型类的实例,但此类具象化实例无法指导类型推断。在 Scala 2.11.0 中,具象化器还可以影响类型推断,帮助 scalac 推断封闭方法应用程序的类型参数,这是 Shapeless 中非常成功地使用的一种方法。更重要的是,通过修复 SI-3346,这种推断指导功能可以同时影响普通方法和隐式转换。但是,请注意,fundep 具象化并不能改变 Scala 的类型推断工作方式,而只是提供了一种将更多类型约束添加到组合中的方法,因此,例如,不可能使用 fundep 具象化器使类型推断从右到左进行。
2) 提取器宏。Scala 2.11.0 中的一个突出新功能是 基于名称的提取器,由 Paul Phillips 实现。并且,像往常一样,当出现一个 Scala 功能时,宏极有可能能够使用它。事实上,借助结构化类型,白盒宏可用于编写提取器,以逐个案例细化提取对象的类型。这是我们在内部用于实现准引用的技术。
3) 宏中的命名和默认参数。严格来说,这并不是此变更日志的一部分,因为此功能在合并到 Scala 2.11.0-RC1 后不久便被还原,原因是一个导致回归的小错误,但我们已获得一个补丁,使宏引擎能够理解宏应用程序中的命名/默认参数。即使代码冻结不允许我们在 Scala 2.11.0 中引入此更改,我们也希望在最早有机会时将其合并到 Scala 2.11.1 中。
4) 类型宏 和 宏注释。Scala 2.11.0 中既不包含类型宏,也不包含宏注释。类型宏极不可能被包含在 Scala 中,但我们仍在考虑宏注释。但是,宏注释通过 宏天堂插件 同时适用于 Scala 2.10.x 和 Scala 2.11.0。
5) @compileTimeOnly。标准库现在提供了一个新的 scala.annotations.compileTimeOnly
注解,它告诉 scalac 其注解对象在类型检查(包括宏展开)后不应被引用。此注解的主要用例是标记仅应与封闭宏调用一起使用的辅助方法,以指示需要特殊处理的该宏调用参数部分(例如,scala/async 中的 await
或 sbt 的基于宏的新 DSL 中的 value
)。例如,scala/async 的 await
标记为 @compileTimeOnly
,仅在 async { ... }
块中才有意义,该块在转换期间将其编译掉,并且由于新注解,在 async
之外使用它是一个编译时错误。
对宏引擎的更改
6) 黑盒/白盒分离。其宏实现使用 scala.reflect.macros.blackbox.Context
(Scala 2.11.0 中的新增内容)的宏称为黑盒,与 2.10.x 中的宏相比,功能有所降低,在 IDE 中有更好的支持,并且更有可能成为 Scala 的一部分。其宏实现使用 scala.reflect.macros.whitebox.Context
(Scala 2.11.0 中的新增内容)或 scala.reflect.macros.Context
(Scala 2.10.x 中唯一的上下文,在 Scala 2.11.0 中已弃用)的宏称为白盒,并且至少具有与 2.10.x 中的宏相同的功能。
7) 宏包。众所周知,当前反射 API(在 Scala 2.10.x 和 Scala 2.11.0 中都有)的路径相关特性使得模块化宏变得困难。有一些 设计模式 有助于克服此困难,但这只会导致样板代码激增。解决此问题的方法之一是完全取消 cake,而这正是我们在 Project Palladium 中追求的目标,但这对于 Scala 2.11.0 来说是一个太大的改变,因此我们想出了一个解决方法,可以在真正的解决方案到来之前缓解此问题。宏包是具有类型为 Context
的单个公共字段的类,并且包内的任何公共方法都可以称为宏实现。然后,此类宏实现可以轻松地调用同一类或其超类的其他方法,而无需携带上下文,因为包已经携带了每个人都可以看到和引用的上下文。这极大地简化了复杂宏的编写和维护。
8) 对宏实现签名的要求放宽。随着准引号的出现,reify 由于过于笨重且不灵活而迅速失宠。为了认识到我们现在允许宏实现的参数和返回类型为 c.Tree
类型,而不是 c.Expr[Something]
类型。不再需要编写巨大的类型签名,然后花费时间和代码行来尝试将宏实现与这些类型对齐。只需输入树并返回树即可 - 样板文件已消失。
9) 宏 def 返回类型的推断正在逐步淘汰。鉴于新的事物方案,宏实现可以返回 c.Tree
而不是 c.Expr[Something]
,因此不再可能从宏 impls 的返回类型中可靠地推断宏 def 的返回类型(如果宏 impl 返回 c.Tree
,那么该树的类型是什么?)。因此,我们正在逐步淘汰这种语言机制。返回 c.Expr[T]
的宏 impls 仍可用于推断其宏 def 的返回类型,但这会产生弃用警告,而尝试使用返回 c.Tree
的宏 impls 来推断宏 def 的返回类型将导致编译错误。
10) 宏扩展类型检查方式的更改。在 Scala 2.10.x 中,宏扩展被检查两次类型:首先针对相应宏 def 的返回类型(称为 innerPt),其次针对从封闭程序派生的预期类型(称为 outerPt)。当返回类型误导类型推断并且宏扩展最终具有不精确类型时,这会导致某些罕见问题。在 Scala 2.11.0 中,类型检查方案已更改。黑盒宏仍然针对 innerPt 和 outerPt 进行类型检查,但白盒宏首先在没有任何预期类型(即针对 WildcardType)的情况下进行类型检查,然后才针对 innerPt 和 outerPt 进行类型检查。
11) 对进出的一切进行复制。不幸的是,反射 API 的核心数据结构(树、符号、类型)本身是可变的,或者可传递地可变。这使得 API 变得脆弱,因为很容易以与未来客户端不兼容的方式无意中更改某人的状态。我们还没有对此有一个完整的解决方案,但我们已经对我们的宏引擎应用了许多保障措施,以在一定程度上遏制变异的可能性。特别是,我们现在复制宏实现的所有参数和返回值,以及 Context.typeCheck
等可能发生变异的 API 的所有输入和输出。
对反射 API 的更改
12) 引入 Universe.internal 和 Context.internal。Scala 2.10.x 反射 API 用户的反馈为我们提供了两个重要的见解。首先,我们公开的某些功能过于底层,并且在公共 API 中非常不合适。其次,某些底层功能对于使重要的宏操作至关重要。为了在一定程度上解决这两个开发向量产生的紧张关系,我们创建了公共 API 的内部子部分,这些子部分:a) 与反射 API 的受祝福部分明确分隔,b) 可供那些知道自己在做什么并希望实现我们想要支持的实际重要用例的人使用。请遵循文档底部的迁移和兼容性说明以了解更多信息。
13) 运行时反射的线程安全性。Scala 2.10.x 中反射中最紧迫的问题是其线程不安全。尝试从多个线程使用运行时反射(例如类型标记)会导致上面记录的奇怪崩溃。我们相信通过在实现的关键位置引入大量锁来修复了 Scala 2.11.0-RC1 中的此问题。一方面,我们目前正在使用的策略不是最优的,因为某些经常使用的操作(例如 Symbol.typeSignature
或 TypeSymbol.typeParams
)隐藏在全局锁之后,但我们计划在未来对其进行优化。另一方面,大多数典型的 API(例如 Type.members
或 Type.<:<
)要么使用线程本地状态,要么根本不需要同步,所以绝对值得尝试。
14) Type.typeArgs。现在,获取给定类型的类型参数非常简单。Scala 2.10.x 中需要模式匹配的内容现在只需简单地调用方法即可。typeArgs
方法还与 typeParams
、paramLists
和 resultType
结合使用,使其非常容易执行常见的类型检查任务。
15) symbolOf[T]。Scala 2.11.0 为非常常见的 typeOf[T].typeSymbol
操作引入了快捷方式,使其更容易找出给定类和对象(注释、标志、可见性等)的元数据。
16) knownDirectSubclasses 被认为已正式损坏。许多尝试遍历类密封层次结构的用户已注意到,ClassSymbol.knownDirectSubclasses
仅在宏调用在 Scala 编译顺序中位于层次结构定义之后时才起作用。例如,如果密封层次结构定义在源文件底部,并且宏应用程序编写在文件顶部,则 knownDirectSubclasses 将返回一个空列表。这是一个根植于 Scala 内部架构的问题,我们无法在近期内提供修复程序。
17) showCode。除了打印类似 Scala 的源代码的 Tree.toString
和打印树内部结构的 showRaw(tree)
,我们现在还有 showCode
,它打印与提供的树对应的可编译 Scala 源代码,这要归功于 Vladimir Nikolaev,他出色地完成了这项工作。我们计划最终用 showCode
替换 Tree.toString
,但在 Scala 2.11.0 中,它们是两种不同的方法。
18) 现在可以在类型和模式模式中进行类型检查。Scala 2.10.x 中非常方便的 Context.typeCheck
和 ToolBox.typeCheck
功能有一个重大不便 - 它仅适用于表达式,并且将某些内容作为类型或模式进行类型检查需要构建虚拟表达式。现在,typeCheck
具有处理该困难情况的模式参数。
19) 反射调用现在支持值类。运行时反射现在可以正确处理方法和构造函数参数中的值类,还可以正确地解包和打包反射调用(如 FieldMirror.get
、FieldMirror.set
和 MethodMirror.apply
)的输入和输出。
20) 反射调用变得更快。借助新引入的 FieldMirror.bind
和 MethodMirror.bind
API,现在可以快速从已存在的镜像创建新镜像,避免进行代价高昂的镜像初始化。在我们的测试中,得益于这些新 API,调用密集型场景的性能提升了 20 倍。
21) Context.introduceTopLevel。 Context.introduceTopLevel
API 曾经在 Scala 2.11.0 的早期里程碑版本中可用,作为通往类型宏的垫脚石,但从最终版本中删除,因为类型宏被拒绝纳入 Scala,并在宏天堂中停止使用。
如何在 2.11.0 中让你的 2.10.x 宏工作
22) 黑盒/白盒。Scala 2.10.x 中的所有宏都是白盒,并将相应地表现,能够在扩展中优化宏定义的已声明返回类型。有关如何在 Scala 2.10.x 中让宏的行为完全像 Scala 2.11.0 中的黑盒宏的信息,请参阅本文档的后续部分。
23) 宏包。Scala 2.11.0 现在识别宏定义右侧对宏实现的某些新形状的引用,在一些非常罕见的情况下,这可能会改变现有代码的编译方式。首先,在这种情况下,不会影响运行时行为 - 如果 Scala 2.10.x 宏定义在 Scala 2.11.0 中编译,那么它将绑定到与之前相同的宏实现。其次,在某些情况下,宏实现引用可能会变得模棱两可并导致编译失败,但可以通过错误消息建议的简单重命名以向后兼容的方式修复此问题。
24) 宏定义返回类型的推断。在 Scala 2.11.0 中,从关联的宏实现推断其返回类型的宏定义将与 Scala 2.10.x 一致地工作。此类宏定义将发出弃用警告,但不会发生编译错误或行为差异。
25) 宏扩展类型检查方式的更改。Scala 2.11.0 更改了用于类型检查白盒宏扩展的预期类型的序列(并且由于 Scala 2.10.x 中的所有宏都是白盒,因此它们都可能受到影响)。在罕见的情况下,当 Scala 2.10.x 宏扩展依赖于预期类型的特定形状以推断其类型参数时,它可能会停止工作。在这种情况下,显式指定此类类型参数将以与 Scala 2.10.x 和 Scala 2.11.0 都兼容的方式解决问题:示例。
26) 进出的一切内容的重复。在 Scala 2.11.0 中,我们始终复制跨越用户空间(宏实现代码)和内核(编译器内部)边界的树,限制了这些树的突变范围。在极少数情况下,Scala 2.10.x 宏可能依赖于此类突变才能正确操作。此类宏将被破坏,必须重写。不过不用太担心,因为我们还没有在野外遇到过此类宏,所以你的宏很可能没问题。
27) Universe.internal 和 Context.internal 的简介。Scala 2.10.x 中提供的以下 51 个 API 已移至反射 cake 的 internal
子模块。有两种方法可以修复这些源代码不兼容性。简单的方法是在 import scala.reflect.runtime.universe._
或 import c.universe._
后编写 import compat._
。困难的方法是简单方法 + 应用从 compat 导入的方法上弃用警告提供的全部迁移建议。
typeTagToManifest | Tree.pos_= | Symbol.isSkolem |
manifestToTypeTag | Tree.setPos | Symbol.deSkolemize |
newScopeWith | Tree.tpe_= | Symbol.attachments |
BuildApi.setTypeSignature | Tree.setType | Symbol.updateAttachment |
BuildApi.flagsFromBits | Tree.defineType | Symbol.removeAttachment |
BuildApi.emptyValDef | Tree.symbol_= | Symbol.setTypeSignature |
BuildApi.This | Tree.setSymbol | Symbol.setAnnotations |
BuildApi.Select | TypeTree.setOriginal | Symbol.setName |
BuildApi.Ident | Symbol.isFreeTerm | Symbol.setPrivateWithin |
BuildApi.TypeTree | Symbol.asFreeTerm | captureVariable |
Tree.freeTerms | Symbol.isFreeType | referenceCapturedVariable |
Tree.freeTypes | Symbol.asFreeType | capturedVariableType |
Tree.substituteSymbols | Symbol.newTermSymbol | singleType |
Tree.substituteTypes | Symbol.newModuleAndClassSymbol | refinedType |
Tree.substituteThis | Symbol.newMethodSymbol | typeRef |
Tree.attachments | Symbol.newTypeSymbol | intersectionType |
Tree.updateAttachment | Symbol.newClassSymbol | polyType |
Tree.removeAttachment | Symbol.isErroneous | existentialAbstraction |
28) knownDirectSubclasses 的官方损坏。除了意识到该 API 的局限性之外,您无法从您的角度在此处执行任何操作。使用 knownDirectSubclasses
的宏将继续在 Scala 2.11.0 中工作,就像它们在 Scala 2.10.x 中所做的那样,没有任何弃用警告。
29) Context.enclosingTree 样式 API 的弃用。现有的封闭树宏 API 同时面临技术和哲学问题,因此我们做出了一个艰难的决定,逐步淘汰它们,在 Scala 2.11.0 中弃用它们,并在 Scala 2.12.0 中删除它们。这些 API 没有直接的替代品,只有新引入的 c.internal.enclosingOwner,它只涵盖了其功能的一个子集。请关注 https://github.com/scala/scala/pull/3354 上的讨论,了解更多信息。
30) 其他弃用。你们中的一些人在构建中启用了 -Xfatal-warnings,因此任何弃用都可能导致编译失败。本指南涵盖了所有有争议的弃用,其余的可以通过直接遵循弃用消息来修复。
31) 删除 resetAllAttrs。resetAllAttrs 是一个非常危险的 API,一开始就不应该公开。这就是为什么我们在没有经过弃用周期的情况下删除它的原因。然而,有一个公开可用的替代品称为 resetLocalAttrs
,它在几乎所有情况下都应该足够,我们建议您使用它。在 resetLocalAttrs
无法解决的特殊情况下,请使用 https://github.com/scalamacros/resetallattrs。
32) 删除 isLocal。Symbol.isLocal
并未按其宣传的那样执行,并且没有办法修复它。因此,我们已在没有任何弃用警告的情况下将其删除,并建议改用 Symbol.isPrivateThis
和/或 Symbol.isProtectedThis
。
33) 删除 isOverride。与 Symbol.isLocal
相同。此方法已损坏且无法修复,因此已从公共 API 中删除。应改用 Symbol.allOverriddenSymbols
(或其新引入的别名 Symbol.overrides
)。
如何在 2.10.x 中使你的 2.11.0 宏工作
34) 准引号。我们不计划将准引号作为 Scala 2.10.x 发行版的一部分发布,但它们仍可以通过宏天堂插件在 Scala 2.10.x 中使用。阅读 天堂文档 以了解使用编译器插件所需的更多信息、二进制兼容性后果以及支持保证。
35) 大多数新功能在 2.10.x 中没有等效项。我们不计划将任何新功能(例如 fundep 具体化或宏包)反向移植到 Scala 2.10.x(除了可能针对运行时反射的线程安全性)。查阅 Scala 2.10.x 的宏天堂路线图 以查看天堂中支持哪些功能。
36) 黑盒/白盒。如果你决心让你的宏成为黑盒,那么让这些宏在 2.10.x 和 2.11.0 中一致工作将需要额外的努力,因为在 2.10.x 中所有宏都是白盒。首先,确保你实际上没有使用任何 白盒功能,否则你必须首先重写你的宏。其次,在从宏实现返回之前,将扩展显式提升为其宏定义所需的类型。(当然,所有这些都不适用于白盒宏。如果你不介意你的宏是白盒,那么你无需执行任何操作即可确保跨兼容性)。
object Macros {
def impl(c: Context) = {
import c.universe._
q"new { val x = 2 }"
}
def foo: Any = macro impl
}
object Test extends App {
// works in Scala 2.10.x and Scala 2.11.0 if foo is whitebox
// doesn't work in Scala 2.11.0 if foo is blackbox
println(Macros.foo.x)
}
object Macros {
def impl(c: Context) = {
import c.universe._
q"(new { val x = 2 }): Any" // note the explicit type ascription
}
def foo: Any = macro impl
}
object Test extends App {
// consistently doesn't work in Scala 2.10.x and Scala 2.11.0
// regardless of whether foo is whitebox or blackbox
println(Macros.foo.x)
}
37) @compileTimeOnly。compileTimeOnly
注解自 Scala 2.10.1 起作为 scala.reflect.internal.compileTimeOnly
在 scala-reflect.jar
中秘密可用(相比之下,在 Scala 2.11.0 中,compileTimeOnly
位于 scala-library.jar
中,名称为 scala.annotations.compileTimeOnly
)。如果你不介意你的 API 用户必须依赖 scala-reflect.jar
,那么即使在 Scala 2.10.x 中也可以继续使用 compileTimeOnly
- 它将在 Scala 2.11.0 中以相同的方式运行。
38) 宏扩展类型检查方式的更改。Scala 2.11.0 更改了用于类型检查白盒宏扩展的预期类型序列(并且由于 Scala 2.10.x 中的所有宏都是白盒宏,因此它们都可能受到影响),这在理论上可能导致类型推断问题。即使从 2.10.x 迁移到 2.11.0,也不太可能成为问题,但反向进行几乎没有可能导致问题。如果您遇到困难,那么就像任何类型推断故障一样,请尝试通过将宏扩展向上转型为所需的类型来提供显式类型注释。
39) Universe.internal 和 Context.internal 的引入。尽管很难想象这是如何工作的,但有可能使用内部 API 编译与 Scala 2.10.x 和 2.11.0 都兼容的宏。非常感谢 Jason Zaugg 为我们指明了道路
// scala.reflect.macros.Context is available both in 2.10 and 2.11
// in Scala 2.11.0 it is deprecated
// and aliased to scala.reflect.macros.whitebox.Context
import scala.reflect.macros.Context
import scala.language.experimental.macros
// provides a source compatibility stub
// in Scala 2.10.x, it will make `import compat._` compile just fine,
// even though `c.universe` doesn't have `compat`
// in Scala 2.11.0, it will be ignored, because `import c.universe._`
// brings its own `compat` in scope and that one takes precedence
private object HasCompat { val compat = ??? }; import HasCompat._
object Macros {
def impl(c: Context): c.Expr[Int] = {
import c.universe._
// enables Tree.setType that's been removed in Scala 2.11.0
import compat._
c.Expr[Int](Literal(Constant(42)) setType definitions.IntTpe)
}
def ultimateAnswer: Int = macro impl
}
40) 使用 macro-compat。 Macro-compat 是一个小型库,它允许您使用 Scala 2.10.x 编译为 Scala 2.11/2 宏 API 编写的宏。它为 Scala 2.10 带来了:黑盒和白盒 Context 类型的类型别名、对宏包的支持、对 2.11 API 的转发器以及对在宏 def 类型签名中使用 Tree 的支持。