隐式解析的变更
本节介绍适用于 Scala 3 中新的 given
和旧式 implicit
的隐式解析更改。隐式解析使用新的算法,该算法更积极地缓存隐式结果以提高性能。还有一些更改会影响语言级别的隐式。
1. 隐式值和隐式方法的结果类型必须显式声明。 仅局部块中的值除外,因为类型仍然可以推断。
class C {
val ctx: Context = ... // ok
/*!*/ implicit val x = ... // error: type must be given explicitly
/*!*/ implicit def y = ... // error: type must be given explicitly
}
val y = {
implicit val ctx = this.ctx // ok
...
}
2. 现在考虑嵌套来选择隐式。 例如,考虑以下场景
def f(implicit i: C) = {
def g(implicit j: C) = {
implicitly[C]
}
}
现在将解析 implicitly
调用到 j
,因为 j
的嵌套深度比 i
更深。 以前,这会导致歧义错误。 由于阴影(隐式被嵌套定义隐藏)导致的隐式搜索失败的可能性不再适用。
3. 包前缀不再对类型的隐式搜索范围做出贡献。 例子
package p
given a: A = A()
object o:
given b: B = B()
type C
a
和 b
在定义 type C
的位置都可见为隐式。 但是,在包 p
之外的 p.o.C
的引用将仅在其隐式搜索范围内包含 b
,而不包含 a
。
更详细地说,以下是构成类型隐式范围的规则
定义: 如果引用指向对象、类、特质、抽象类型、不透明类型别名或匹配类型别名,则该引用是锚点。 对包和包对象的引用仅在 -source:3.0-migration
下是锚点。 不透明类型别名仅在别名可见的范围之外才算作锚点。
定义: 类型T的锚点是一组定义如下引用
- 如果T是锚点的引用,则T本身加上,如果T的形式为P#A,则P的锚点。
- 如果T是U的别名,则U的锚点。
- 如果T是类型参数的引用,则其两个边界的锚点的并集。
- 如果T是单例引用,则其底层类型的锚点,加上,如果T的形式为(P#x).type,则P的锚点。
- 如果T是静态对象o的 this 类型o.this,则指向该对象的术语引用o.type的锚点,
- 如果T是其他 this 类型P.this.type,则P的锚点。
- 如果T是其他类型,则T的每个组成类型的锚点的并集。
定义: 类型T的隐式范围是最小的术语引用集S,使得
- 如果T是类的引用,则S包含对该类的伴生对象的引用(如果存在),以及T的所有父类的隐式范围。
- 如果T 是对某个对象的引用,S 包含T 本身以及T 所有父类隐式作用域。
- 如果T 是对名为A 的不透明类型别名的引用,S 包含对在与类型相同作用域中定义的对象A 的引用(如果存在),以及T 底层类型或边界的隐式作用域。
- 如果T 是对名为A 的抽象类型或匹配类型别名的引用,S 包含对在与类型相同作用域中定义的对象A 的引用(如果存在),以及T 给定边界的隐式作用域。
- 如果T 是对形式为p.A 的锚点的引用,则S 还包含路径p 上的所有术语引用。
- 如果T 是其他类型,S 包含T 所有锚点的隐式作用域。
4. 模糊错误的处理方式已更改。如果在隐式搜索的某个递归步骤中遇到模糊,则将模糊传播到调用者。
示例:假设您有以下定义
class A
class B extends C
class C
implicit def a1: A
implicit def a2: A
implicit def b(implicit a: A): B
implicit def c: C
以及查询 implicitly[C]
。
此查询现在将被归类为模糊。这是有道理的,毕竟有两个可能的解决方案,b(a1)
和 b(a2)
,它们之间没有优劣之分,并且都优于第三个解决方案 c
。相比之下,Scala 2 会拒绝对 A
的搜索,因为它认为它是模糊的,随后会将查询 b(implicitly[A])
归类为正常失败,这意味着 c
将被选为解决方案!
Scala 2 在模糊方面的行为有些令人费解,它被用来实现隐式解析中“否定”搜索的类似功能,其中查询 Q1
失败,如果另一个查询 Q2
成功,而 Q1
成功,如果 Q2
失败。随着新清理后的行为,这些技术不再有效。但现在有一个新的特殊类型 scala.util.NotGiven
,它直接实现了否定。对于任何查询类型 Q
,NotGiven[Q]
成功当且仅当对 Q
的隐式搜索失败。
5. 发散错误的处理方式也已更改。发散隐式被视为正常失败,之后仍然会尝试其他备选方案。这也有道理:遇到发散隐式意味着我们假设在相应路径上找不到有限的解决方案,但仍然可以尝试其他路径。相比之下,Scala 2 中的大多数(但并非全部)发散错误会终止整个隐式搜索。
6. Scala 2 对具有按名参数的隐式转换的优先级低于具有按值参数的隐式转换。Scala 3 取消了这种区别。因此,以下代码片段在 Scala 3 中将是模糊的
implicit def conv1(x: Int): A = new A(x)
implicit def conv2(x: => Int): A = new A(x)
def buzz(y: A) = ???
buzz(1) // error: ambiguous
7. 从一组重载或隐式备选方案中选择最具体备选方案的规则已得到改进,以考虑上下文参数。在其他条件相同的情况下,使用一些上下文参数的备选方案被认为不如不使用任何上下文参数的备选方案具体。如果两个备选方案都使用上下文参数,我们将尝试像它们是具有常规参数的方法一样在它们之间进行选择。SLS §6.26.3 中的以下段落受此更改的影响
原始版本
如果 A 相对于 B 的权重大于 B 相对于 A 的权重,则备选方案 A 比备选方案 B 更具体。
修改后的版本
如果备选方案 A 比备选方案 B 更具体,则
- A 相对于 B 的权重大于 B 相对于 A 的权重,或者
- 相对权重相同,并且 A 不接受隐式参数,而 B 接受隐式参数,或者
- 相对权重相同,A 和 B 都接受隐式参数,并且如果将任一备选方案中的所有隐式参数替换为常规参数,则 A 比 B 更具体。
8. 之前基于继承深度的隐式歧义消除规则已得到改进,使其具有传递性。传递性对于保证搜索结果与编译顺序无关至关重要。以下是一个之前规则违反传递性的场景
class A extends B
object A { given a ... }
class B
object B extends C { given b ... }
class C { given c }
这里,a
比 b
更具体,因为伴随类 A
是伴随类 B
的子类。此外,b
比 c
更具体,因为 object B
扩展了类 C
。但是,a
不比 c
更具体。这意味着如果 a, b, c
都是适用的隐式参数,那么它们比较的顺序会影响结果。如果我们首先比较 b
和 c
,我们会保留 b
并丢弃 c
。然后,比较 a
和 b
,我们会保留 a
。但是,如果我们首先比较 a
和 c
,我们会遇到歧义错误。
新规则如下:如果在 A
中定义的隐式 a
比在 B
中定义的隐式 b
更具体,则
A
扩展了B
,或者A
是一个对象,并且A
的伴随类扩展了B
,或者A
和B
是对象,B
不从基类继承任何隐式成员 (*),并且A
的伴随类扩展了B
的伴随类。
条件 (*) 是新的。它对于确保定义的关系具有传递性是必要的。
[//]: # todo: 扩展精确规则
9. 以下更改目前在 -source future
中启用
隐式解析现在避免生成会导致运行时无限循环的递归 given。以下是一个示例
object Prices {
opaque type Price = BigDecimal
object Price{
given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
}
}
以前,隐式解析会将 summon
解析为 Price
中的 given,从而导致无限循环(在这种情况下会发出警告)。我们现在使用 BigDecimal
中的底层 given。我们通过为隐式搜索添加以下规则来实现这一点
- 在检查形式为
G
的given
定义的实现时进行隐式搜索时given ... = ....
丢弃所有导致返回
G
或返回与G
具有相同所有者且在源代码中比G
靠后的 given 的搜索结果。
新行为目前在 source.future
中启用,最早将在 Scala 3.6 中启用。对于更早的源代码版本,行为如下
- Scala 3.3:无变化
- Scala 3.4:在行为将在 3.future 中更改的地方发出警告。
- Scala 3.5:在行为将在 3.future 中更改的地方发出错误。
旧式隐式定义不受此更改的影响。