Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: passing a contravariant type as an implicit parameter does not choose the nearest supertype?

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.

like image 578
cubic lettuce Avatar asked Jan 01 '23 20:01

cubic lettuce


2 Answers

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
like image 128
Andrey Tyukin Avatar answered Apr 27 '23 02:04

Andrey Tyukin


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.

like image 20
Luis Miguel Mejía Suárez Avatar answered Apr 27 '23 02:04

Luis Miguel Mejía Suárez