This is a simple example:
object CoerciveCovariance {
trait Cov[+T]
def cast[A, B](v: Cov[A])(
implicit
ev: A <:< B
) = {
v: Cov[B]
}
}
It doesn't compile:
CoerciveCovariance.scala:11:5: Found: (v : xxx.CoerciveCovariance.Cov[A])
Required: xxx.CoerciveCovariance.Cov[B]
one error found
Is it very hard to make to compiler to figure out the missing coercive upcasting from Cov[A]
to Cov[B]
? Why is it not the default behaviour?
Because type inference and implicit resolution are different. <:
and +
belong to type inference, <:<
belongs to implicit resolution.
They make impact to each other. Indeed, type inference makes impact to implicit resolution
trait TC[A]
implicit val int: TC[Int] = null
def foo[A](a: A)(implicit tc: TC[A]) = null
foo(1) // compiles
foo("a") // doesn't compile
Here firstly type A
is inferred to be Int
(or String
) and then it's checked that there is an implicit for Int
(and no implicit for String
).
Similarly, implicit resolution makes impact to type inference
trait TC[A, B]
implicit val int: TC[Int, String] = null
def foo[A, B](a: A)(implicit tc: TC[A, B]): B = ???
val x = foo(1)
// checking the type
x: String // compiles
Here the type String
was inferred from the type class having the only instance.
So type inference and implicit resolution make impact to each other but are different.
If A <: B
then A <:< B
def test[A <: B, B] = implicitly[A <:< B] // compiles
but if A <:< B
then not necessarily A <: B
def checkSubtype[A <: B, B] = null
def test[A, B](implicit ev: A <:< B) = checkSubtype[A, B] // doesn't compile
<:
is checked by the compiler according to the spec https://scala-lang.org/files/archive/spec/2.13/03-types.html#conformance
<:<
is just a type class
sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
with the only instance
object <:< {
implicit def refl[A]: A =:= A = singleton.asInstanceOf[A =:= A] // the instance
}
sealed abstract class =:=[From, To] extends (From <:< To) with Serializable
So <:<
doesn't have many properties of an order. By default there is no transitivity
def test[A, B, C](implicit ev: A <:< B, ev1: B <:< C) = implicitly[A <:< C] // doesn't compile
no antisymmetry
def test[A, B](implicit ev: A <:< B, ev1: B <:< A) = implicitly[A =:= B] // doesn't compile
no monotonicity
def test[A, B, F[+_]](implicit ev: A <:< B) = implicitly[F[A] <:< F[B]] // doesn't compile
Although starting from Scala 2.13 the following methods are defined in the standard library
sealed abstract class <:<[-From, +To] extends (From => To) with Serializable {
def andThen[C](r: To <:< C): From <:< C = {
type G[-T] = T <:< C
substituteContra[G](r)
}
def liftCo[F[+_]]: F[From] <:< F[To] = {
type G[+T] = F[From] <:< F[T]
substituteCo[G](implicitly[G[From]])
}
}
object <:< {
def antisymm[A, B](implicit l: A <:< B, r: B <:< A): A =:= B = singleton.asInstanceOf[A =:= B]
}
but they do not define implicits. So if you need these properties you can define transitivity
implicit def trans[A, B, C](implicit ev: A <:< B, ev1: B <:< C): A <:< C = ev.andThen(ev1)
def test[A, B, C](implicit ev: A <:< B, ev1: B <:< C) = implicitly[A <:< C] // compiles
Antisymmetry is trickier
implicit def antisym[A, B](implicit ev: A <:< B, ev1: B <:< A): (A =:= B) = <:<.antisymm[A, B]
def test[A, B](implicit ev2: A <:< B, ev3: B <:< A) = implicitly[A =:= B] // doesn't compile
If you resolve implicits manually ... = implicitly[A =:= B](antisym[A, B])
, you'll see the reason (although implicitly[A =:= B](antisym[A, B](ev2, ev3))
works)
ambiguous implicit values:
both method antisym in object App of type [A, B](implicit ev: A <:< B, ev1: B <:< A): A =:= B
and value ev2 of type A <:< B
match expected type A <:< B
So you have to resolve this ambiguity prioritizing implicits. You can't decrease the priority of implicit parameter ev2
. So you have to decrease the priority of antisym
, which is your implicit in the current scope, you can't put it to the implicit scope (companion object etc.). The only way I found is with shapeless.LowPriority
implicit def antisym[A, B](implicit ev: A <:< B, ev1: B <:< A, lp: LowPriority): (A =:= B) = <:<.antisymm[A, B]
def test[A, B](implicit ev2: A <:< B, ev3: B <:< A) = implicitly[A =:= B] // compiles
Similarly you can define monotonicity
implicit def liftCo[A, B, F[+_]](implicit ev: A <:< B): F[A] <:< F[B] = ev.liftCo[F]
def test[A, B, F[+_]](implicit ev: A <:< B) = implicitly[F[A] <:< F[B]] // compiles
def test1[A, B](implicit ev: A <:< B) = implicitly[Cov[A] <:< Cov[B]] // compiles
But if you put all instances into the scope you'll have compile-time Stackoverflow
implicit def liftCo[A, B, F[+_]](implicit ev: A <:< B): F[A] <:< F[B] = ev.liftCo[F]
implicit def trans[A, B, C](implicit ev: A <:< B, ev1: B <:< C): A <:< C = ev.andThen(ev1)
implicit def antisym[A, B](implicit ev: A <:< B, ev1: B <:< A, lp: LowPriority): (A =:= B) = <:<.antisymm[A, B]
def test[A, B, F[+_]](implicit ev: A <:< B) = implicitly[F[A] <:< F[B]] // doesn't compile, Stackoverflow
So I guess you see why those methods are not defined as implicits by default. This would pollute the implicit scope.
More about the difference <:
vs. <:<
https://blog.bruchez.name/posts/generalized-type-constraints-in-scala/
Besides (compile-time) type class <:<
there is also (runtime) method <:<
from scala-reflect
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
def checkSubtype[A, B]: Unit = macro checkSubtypeImpl[A, B]
def checkSubtypeImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
import c.universe._
println(weakTypeOf[A] <:< weakTypeOf[B])
q"()"
}
type A <: B
type B
checkSubtype[A, B] // scalac: true // scalacOptions += "-Ymacro-debug-lite"
type A
type B
checkSubtype[A, B] // scalac: false
Scala 2.13.10.
Ensure arguments to generic method have same type in trait method
What is the implicit resolution chain of `<:<`
Type parameter under self-type doesn't conform to upper bound despite evidence