函数调用和隐式搜索之间的scala重载解决差异

scala overloading resolution differences between function calls and implicit search

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@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 的实例,为什么当 A 的实例在范围内时,编译器会考虑 BC 的实例。如果解析逻辑与函数调用相同,就不会有歧义。

编辑 2编辑 1 已被删除,因为我在那里写的断言是错误的。

作为对评论的回应,我添加了另一个测试以查看删除 implicit val c: C 时会发生什么。在那种情况下,尽管调用者要求 A.

的实例,编译器也不会抱怨并选择 implicit val b: B
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 编译器实现的当前隐式解析逻辑,此代码段适用于 SetList,但不适用于 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@3958db82

scala> Parser[Vector[Int]]
val res1: read.Parser[Vector[Int]] = read.Parser$$anonfun$pursue@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]

相反,如果 implicit 的重载决策逻辑类似于函数调用的逻辑,则可以正常工作。

编辑 3:喝了很多咖啡后我注意到,与我上面所说的相反,编译器决定调用哪些重载函数和调用哪些重载函数的方式没有区别重载隐式选择。

在函数调用的情况下:从所有函数重载中,参数类型可分配给参数类型,编译器选择一个函数参数类型可分配给所有其他函数.如果没有函数满足该要求,则会抛出编译错误。

在隐式拾取的情况下:从范围内的所有隐式中,隐式类型可分配给所要求的类型,编译器选择一个声明的类型可分配给所有其他类型。如果没有 implicit 满足该要求,则会抛出编译错误。

我的错误是我没有注意到可分配性的反转。 反正我上面提出的解析逻辑(求什么就给我什么)也不是完全错误的。它解决了我提到的特殊情况。但是对于大多数用例,由 scala 编译器(以及我想,所有其他支持类型 类 的语言)实现的逻辑更好。

如问题的 编辑 3 部分所述,编译器决定调用哪些重载函数的方式与选择哪些重载隐式函数的方式有相似之处。在这两种情况下,编译器都会执行两个步骤:

  1. 过滤掉所有不可分配的备选方案。
  2. 从剩余的备选方案中选择最具体的,如果超过一个则投诉。

在函数调用的情况下,最具体的选择是参数类型最具体的函数;在隐式选择的情况下,是具有最具体声明类型的实例。

但是,如果两种情况的逻辑完全相同,那么为什么问题的例子给出了不同的结果?因为存在差异:确定哪些备选方案通过第一步的可分配性要求是 oposite。 在函数调用的情况下,在第一步之后仍然是参数类型比参数类型更 generic 的函数;并且在隐式选择的情况下,保留声明类型比请求类型更具体的实例。

上面的话足以回答问题本身,但并没有给出激发它的问题的解决方案,即:如何强制编译器选择声明类型与声明类型完全相同的隐式实例召唤类型?答案是:将隐式实例包装在非变体包装器中。