Why does the following code does not pick up the implicit val with the nearest supertype?
class A
class B extends A
trait TC[-T] { def show(t: T): String }
implicit val showA = new TC[A] { def show(a: A): String = "it's A" }
implicit val showB = new TC[B] { def show(b: B): String = "it's B" }
def doit[X](x: X)(implicit tc: TC[X]): Unit = println(tc.show(x))
doit(new A) // "it's A" as expected
doit(new B) // "it's A" ... why does this not give "it's B" ???
If you make TC
invariant (i.e., trait TC[T] (...)
), then it works fine and doit(new B)
returns "it's B" as expected.
By adding another implicit for type Any
, this issue is even extremer:
class A
class B extends A
trait TC[-T] { def show(t: T): String }
implicit val showA = new TC[A] { def show(a: A): String = "it's A" }
implicit val showB = new TC[B] { def show(b: B): String = "it's B" }
implicit val showAny = new TC[Any] { def show(x: Any): String = "it's Any" }
def doit[X](x: X)(implicit tc: TC[X]): Unit = println(tc.show(x))
doit(new A) // "it's Any" ... why does this not give "it's A" ???
doit(new B) // "it's Any" ... why does this not give "it's B" ???
And again it work's fine if TC
is invariant.
What's going on here, and how to solve it?
My goal is to have a contravariant TC
that implicitly chooses the nearest suitable supertype.
Since TC[-T]
is contravariant in its type argument, TC[A]
is a subtype of TC[B]
, and is therefore considered to be more "specific". That's a well known (and somewhat controversial) design decision, which essentially means that implicit resolution with contravariance sometimes behaves quite unexpectedly.
Workaround 1: prioritizing implicits using inheritance
Here is how you could use the inheritance and the "LowPriority-*-Implicits" pattern:
class A
class B extends A
class C extends B
class D extends C
trait TC[-T] { def show(t: T): String }
trait LowPriorityFallbackImplicits {
implicit def showA[X <: A]: TC[X] =
new TC[A] { def show(a: A): String = "it's A" }
}
object TcImplicits extends LowPriorityFallbackImplicits {
implicit def showC[X <: C]: TC[X] =
new TC[C] { def show(c: C): String = "it's C" }
}
def doit[X](x: X)(implicit tc: TC[X]): Unit = println(tc.show(x))
import TcImplicits._
doit(new A)
doit(new B)
doit(new C)
doit(new D)
Now it picks the most specific one in all cases:
it's A
it's A
it's C
it's C
Workaround 2: invariant helper trait
Here is how you can forcibly swap the implicits in your particular example by introducing a helper trait that is invariant in the type argument:
class A
class B extends A
trait TC[-T] { def show(t: T): String }
val showA = new TC[A] { def show(a: A): String = "it's A" }
val showB = new TC[B] { def show(b: B): String = "it's B" }
trait TcImplicit[X] { def get: TC[X] }
implicit val showAImplicit = new TcImplicit[A] { def get = showA }
implicit val showBImplicit = new TcImplicit[B] { def get = showB }
def doit[X](x: X)(implicit tc: TcImplicit[X]): Unit = println(tc.get.show(x))
doit(new A)
doit(new B)
prints
it's A
it's B
If there are several eligible arguments which match the implicit parameter's type, a most specific one will be chosen using the rules of static overloading resolution. If the parameter has a default argument and no implicit argument can be found the default argument is used.
-Scala Language Specification - version 2.12
Basically, because you make your TypeClass contravariant, it means that TC[Any] <:< TC[A] <:< TC[B]
(Where <:< means subtype).
And for that TC[Any]
is considered to be the most specific one.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With