scala 2.13.3 编译器确定要调用哪个重载函数的方式与选择哪个重载隐式函数的方式有所不同。
object Thing {
trait A;
trait B extends A;
trait C extends A;
def f(a: A): String = "A"
def f(b: B): String = "B"
def f(c: C): String = "C"
implicit val a: A = new A {};
implicit val b: B = new B {};
implicit val c: C = new C {};
}
import Thing._
scala> f(new B{})
val res1: String = B
scala> implicitly[B]
val res2: Thing.B = Thing$$anon$2@2f64f99f
scala> f(new A{})
val res3: String = A
scala> implicitly[A]
^
error: ambiguous implicit values:
both value b in object Thing of type Thing.B
and value c in object Thing of type Thing.C
match expected type Thing.A
如我们所见,重载解析适用于函数调用,但不适用于隐式选择。为什么没有像函数调用那样选择val a
提供的隐式?如果调用方请求A
的实例,编译器为什么考虑B
的实例,并在A
的实例在范围内时C
。如果解析逻辑与函数调用的解析逻辑相同,则不会有歧义。
编辑 2:编辑 1被删除了,因为我在那里写的断言是错误的。
作为对评论的回应,我添加了另一个测试,以查看删除implicit val c: C
时会发生什么。在这种情况下,编译器不会抱怨并选择implicit val b: B
尽管调用者要求A
实例。
object Thing {
trait A { def name = 'A' };
trait B extends A { def name = 'B' };
trait C extends A { def name = 'C' };
def f(a: A): String = "A"
def f(b: B): String = "B"
implicit val a: A = new A {};
implicit val b: B = new B {};
}
import Thing._
scala> f(new A{})
val res0: String = A
scala> implicitly[A].name
val res3: Char = B
因此,隐式的重载分辨率与函数调用的差异比我预期的要大。 无论如何,我仍然找不到为什么 scala 的设计者决定对函数和隐式重载应用不同的分辨率逻辑。(编辑:后来注意到原因)。
让我们看看在现实世界的例子中会发生什么。 假设我们正在做一个 Json 解析器,将 Json 字符串直接转换为 scala 抽象数据类型,并且我们希望它支持许多标准集合。 负责解析可迭代集合的代码段如下所示:
trait Parser[+A] {
def parse(input: Input): ParseResult;
///// many combinators here
}
implicit def summonParser[T](implicit parserT: Parser[T]) = parserT;
/** @tparam IC iterator type constructor
* @tparam E element's type */
implicit def iterableParser[IC[E] <: Iterable[E], E](
implicit
parserE: Parser[E],
factory: IterableFactory[IC]
): Parser[IC[E]] = '[' ~> skipSpaces ~> (parserE <~ skipSpaces).repSepGen(coma <~ skipSpaces, factory.newBuilder[E]) <~ skipSpaces <~ ']';
这需要元素的Parser[E]
和构造类型参数指定的集合的IterableFactory[IC]
。 因此,我们必须在隐式作用域中为我们想要支持的每种集合类型放置一个IterableFactory
实例。
implicit val iterableFactory: IterableFactory[Iterable] = Iterable
implicit val setFactory: IterableFactory[Set] = Set
implicit val listFactory: IterableFactory[List] = List
使用由 scala 编译器实现的当前隐式解析逻辑,此代码段适用于Set
和List
,但不适用于Iterable
。
scala> def parserInt: Parser[Int] = ???
def parserInt: read.Parser[Int]
scala> Parser[List[Int]]
val res0: read.Parser[List[Int]] = read.Parser$$anonfun$pursue$3@3958db82
scala> Parser[Vector[Int]]
val res1: read.Parser[Vector[Int]] = read.Parser$$anonfun$pursue$3@648f48d3
scala> Parser[Iterable[Int]]
^
error: could not find implicit value for parameter parserT: read.Parser[Iterable[Int]]
原因是:
scala> implicitly[IterableFactory[Iterable]]
^
error: ambiguous implicit values:
both value listFactory in object IterableParser of type scala.collection.IterableFactory[List]
and value vectorFactory in object IterableParser of type scala.collection.IterableFactory[Vector]
match expected type scala.collection.IterableFactory[Iterable]
相反,如果隐式的重载解析逻辑类似于函数调用的逻辑,这将正常工作。
编辑3:喝了很多杯咖啡后,我注意到,与我上面所说的相反,编译器决定调用哪些重载函数和选择哪些重载隐式函数的方式没有区别。
在函数调用的情况下:从所有函数重载中,使参数的类型可分配给参数的类型,编译器选择一个函数的参数类型可分配给所有其他函数的类型。如果没有函数满足此要求,则会引发编译错误。
在隐式拾取的情况下:从所有隐式作用域中,使得隐式的类型可分配给所请求的类型,编译器选择声明的类型可分配给所有其他类型。如果没有隐式满足,则会引发编译错误。
我的错误是我没有注意到可分配性的反转。 无论如何,我上面提出的解决逻辑(给我我要求的内容)并非完全错误。它解决了我提到的特殊情况。但是对于大多数用例,scala编译器(以及,我想,所有其他支持类型类的语言)实现的逻辑更好。
如问题的编辑 3部分所述,编译器决定调用哪些重载函数和选择哪些重载隐式函数的方式之间存在相似性。在这两种情况下,编译器都会执行两个步骤:
- 筛选出所有不可分配的备选方案。
- 从其余的替代方案中选择最具体的,或者如果有多个替代方案,则抱怨。
在函数调用的情况下,最具体的替代方法是具有最具体的参数类型的函数;在隐式选择的情况下,是具有最具体的声明类型的实例。
但是,如果两种情况下的逻辑完全相同,那么为什么问题的例子给出了不同的结果呢?因为有一个区别:确定哪些备选方案通过第一步的可分配性要求是相反的。 在函数调用的情况下,在第一步之后,保留参数类型比参数类型更通用的函数;在隐式选取的情况下,保留其声明类型比请求类型更具体的实例。
以上的话足以回答问题本身,但并没有给出一个解决方案,即:如何强制编译器选择声明类型与召唤类型完全相同的隐式实例?答案是:将隐式实例包装在非变体包装器中。