Implicit Resolution with Contravariance

2020-06-16 01:48发布

问题:

Given classes Parent and Child.

scala> class Parent
defined class Parent

scala> class Child extends Parent
defined class Child

Define implicits for Parent and Child

scala> implicit val a = new Parent
a: Parent = Parent@5902f207

scala> implicit val b = new Child
b: Child = Child@3f7d8bac

Use implicitly to find out which implicit gets resolved.

scala> implicitly[Child] 
res1: Child = Child@3f7d8bac

illustration of my understanding:

         Parent
           |
         Child -- implicit resolution gets the most specific, lowest sub-type

Now, let's use a contravariant type.

scala> trait A[-T]
defined trait A

scala> case class Concrete[T]() extends A[T]
defined class Concrete

Then define a Parent and Child class.

scala> class Parent
defined class Parent

scala> class Kid extends Parent
defined class Kid

Create implicits for them too.

scala> implicit val x = Concrete[Parent]
x: Concrete[Parent] = Concrete()

scala> implicit val y = Concrete[Kid]
y: Concrete[Kid] = Concrete()

scala> implicitly[A[Parent]]
res1: A[Parent] = Concrete()

scala> implicitly[A[Kid]]
    <console>:21: error: ambiguous implicit values:
     both value x of type => Concrete[Parent]
     and value y of type => Concrete[Kid]
     match expected type A[Kid]
                  implicitly[A[Kid]]
                        ^

In the first example (without contravariance), Scala was able to resolve the implicit Child for implicitly[Parent]. It seems to me that it's picking the lowest sub-type.

However, when using contravariance, the behavior changes. Why?

回答1:

Your implicits are typed Concrete and that is invariant here.

Try either

case class Concrete[-T]() extends A[T]

or

implicit val x: A[Parent] = Concrete[Parent]

More words:

Implicits (values or views) should have an explicit type so you're never surprised by the inferred type. Picking the implicit is all about the type.

It picks one of your implicits using the same rules as the overloading resolution conversion that is used to pick alternatives of an overloaded symbol.

For simple values (not function calls), that comes down to conformance or subtyping.

There is also the rule that a definition in a "derived type" (usually a subclass) is preferred.

Here is a test you can do using only commonly available household materials:

scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._, definitions._ also imported    **
** Try  :help, :vals, power.<tab>           **

scala> trait A[-T]
defined trait A

scala> case class Concrete[T](i: Int) extends A[T]
defined class Concrete

scala> class Parent ; class Kid extends Parent
defined class Parent
defined class Kid

// it will pick X if X isAsSpecific as Y but not conversely
scala> typer.infer.isAsSpecific(typeOf[Concrete[Kid]],typeOf[Concrete[Parent]])
res0: Boolean = false

scala> typer.infer.isAsSpecific(typeOf[Concrete[Parent]],typeOf[Concrete[Kid]])
res1: Boolean = false

scala> case class Concrete[-T](i: Int) extends A[T]
defined class Concrete

scala> typer.infer.isAsSpecific(typeOf[Concrete[Kid]],typeOf[Concrete[Parent]])
res2: Boolean = false

scala> typer.infer.isAsSpecific(typeOf[Concrete[Parent]],typeOf[Concrete[Kid]])
res3: Boolean = true

Edit:

Another view of why it matters what type you're testing:

scala> trait A[-T]
defined trait A

scala> case class Concrete[T](i: Int) extends A[T]  // invariant
defined class Concrete

scala> class Parent ; class Kid extends Parent
defined class Parent
defined class Kid

scala> implicitly[Concrete[Parent] <:< Concrete[Kid]]
<console>:13: error: Cannot prove that Concrete[Parent] <:< Concrete[Kid].
              implicitly[Concrete[Parent] <:< Concrete[Kid]]
                        ^

scala> implicit val x: Concrete[Parent] = Concrete[Parent](3)  // the inferred type
x: Concrete[Parent] = Concrete(3)

scala> implicit val y  = Concrete[Kid](4)
y: Concrete[Kid] = Concrete(4)

// both values conform to A[Kid] (because A is contravariant)
// but when it puts x and y side-by-side to see which is more specific,
// it no longer cares that you were looking for an A.  All it knows is
// that the values are Concrete.  The same thing happens when you overload
// a method; if there are two candidates, it doesn't care what the expected
// type is at the call site or how many args you passed.

scala> implicitly[A[Kid]]
<console>:15: error: ambiguous implicit values:
 both value x of type => Concrete[Parent]
 and value y of type => Concrete[Kid]
 match expected type A[Kid]
              implicitly[A[Kid]]
                        ^

Give them explicit types and the variance of Concrete won't matter. You always supply explicit types for your implicits, right? Just like retronym tells us to?

scala> implicit val x: A[Parent] = Concrete[Parent](3)
x: A[Parent] = Concrete(3)

scala> implicit val y: A[Kid] = Concrete[Kid](4)
y: A[Kid] = Concrete(4)

scala> implicitly[A[Kid]]
res2: A[Kid] = Concrete(3)


标签: scala