In Scala there\'s a class <:<
that witnesses a type constraint. From Predef.scala
:
sealed abstract class <:<[-From, +To]
Suppose we've got the following simple type hierarchy:
trait Foo
trait Bar extends Foo
We can ask for proof that Bar
extends Foo
:
val ev = implicitly[Bar <:< Foo]
If we run this in a console with -Xprint:typer
, we'll see the following:
private[this] val ev: <:<[Bar,Foo] =
scala.this.Predef.implicitly[<:<[Bar,Foo]](scala.this.Predef.$conforms[Bar]);
So the compiler has picked $conforms[Bar]
as the implicit value we've asked for. Of course this value has type Bar <:< Bar
, but because <:<
is covariant in its second type parameter, this is a subtype of Bar <:< Foo
, so it fits the bill.
(There's some magic involved here in the fact that the Scala compiler knows how to find subtypes of the type it's looking for, but it's a fairly generic mechanism and isn't too surprising in its behavior.)
Now suppose we ask for proof that Bar
extends String
:
val ev = implicitly[Bar <:< String]
If you turn on -Xlog-implicits
, you'll see this:
:9: $conforms is not a valid implicit value for <:<[Bar,String] because:
hasMatchingSymbol reported error: type mismatch;
found : <:<[Bar,Bar]
required: <:<[Bar,String]
val ev = implicitly[Bar <:< String]
^
:9: error: Cannot prove that Bar <:< String.
val ev = implicitly[Bar <:< String]
^
The compiler tries the Bar <:< Bar
again, but since Bar
isn't a String
, this isn't a subtype of Bar <:< String
, so it's not what we need. But $conforms
is the only place the compiler can get <:<
instances (unless we've defined our own, which would be dangerous), so it quite properly refuses to compile this nonsense.
To address your second question: the <:<[-From, +To]
class is necessary because we need the type parameters for this type class to be useful. The singleton Any <:< Any
value could just as well be defined as an object—the decision to use a val
and an anonymous class is arguably a little simpler, but it's an implementation detail that you shouldn't ever need to worry about.