I can see in the API docs for Predef that they're subclasses of a generic function type (From) => To, but that's all it says. Um, what? Maybe there's documentation somewhere, but search engines don't handle "names" like "<:<" very well, so I haven't been able to find it.
Follow-up question: when should I use these funky symbols/classes, and why?
These are called generalized type constraints. They allow you, from within a type-parameterized class or trait, to further constrain one of its type parameters. Here's an example:
case class Foo[A](a:A) { // 'A' can be substituted with any type
// getStringLength can only be used if this is a Foo[String]
def getStringLength(implicit evidence: A =:= String) = a.length
}
The implicit argument evidence
is supplied by the compiler, iff A
is String
. You can think of it as a proof that A
is String
--the argument itself isn't important, only knowing that it exists. [edit: well, technically it actually is important because it represents an implicit conversion from A
to String
, which is what allows you to call a.length
and not have the compiler yell at you]
Now I can use it like so:
scala> Foo("blah").getStringLength
res6: Int = 4
But if I tried use it with a Foo
containing something other than a String
:
scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]
You can read that error as "could not find evidence that Int == String"... that's as it should be! getStringLength
is imposing further restrictions on the type of A
than what Foo
in general requires; namely, you can only invoke getStringLength
on a Foo[String]
. This constraint is enforced at compile-time, which is cool!
<:<
and <%<
work similarly, but with slight variations:
A =:= B
means A must be exactly BA <:< B
means A must be a subtype of B (analogous to the simple type constraint <:
)A <%< B
means A must be viewable as B, possibly via implicit conversion (analogous to the simple type constraint <%
)This snippet by @retronym is a good explanation of how this sort of thing used to be accomplished and how generalized type constraints make it easier now.
ADDENDUM
To answer your follow-up question, admittedly the example I gave is pretty contrived and not obviously useful. But imagine using it to define something like a List.sumInts
method, which adds up a list of integers. You don't want to allow this method to be invoked on any old List
, just a List[Int]
. However the List
type constructor can't be so constrainted; you still want to be able to have lists of strings, foos, bars, and whatnots. So by placing a generalized type constraint on sumInts
, you can ensure that just that method has an additional constraint that it can only be used on a List[Int]
. Essentially you're writing special-case code for certain kinds of lists.