Try introducing two type parameters A
and B
and then relate them with a generalised type constraint A =:= B
case class Herd[A <: Animal, B](x: A, y: B*)(implicit ev: A =:= B)
Herd(Lion()) // ok
Herd(Lion(), Lion()) // ok
Herd(Cat(), Lion()) // compile-time error
what exactly is =:=
Consider the following method with two type parameters A
and B
where we aim to convey that they should be equal or at least A
should be a subtype of B
scala> def f[A, B](a: A, b: B): B = {
| val x: B = a
| x
| }
val x: B = a
^
On line 2: error: type mismatch;
found : a.type (with underlying type A)
required: B
The two type parameters are totally unrelated in the above definition and the method body cannot influence the type inference of type parameters so it errors. Now let's try to relate them with a type bound A <: B
scala> def f[A <: B, B](a: A, b: B): B = {
| val x: B = a
| x
| }
def f[A <: B, B](a: A, b: B): B
So this compiles, however compiler will always try to satisfy the type bounds by calculating the least upper bound of the given arguments
scala> f(Lion(), Dog())
val res32: Product with Animal with java.io.Serializable = Lion(...)
We need something more to get around compiler's tendency to deduce a least upper bound, and this is where generalised type equality constraint comes into play
scala> def f[A <: B, B](a: A, b: B)(implicit ev: A =:= B): B = {
| val x: B = a
| x
| }
def f[A <: B, B](a: A, b: B)(implicit ev: A =:= B): B
scala> f(Lion(), Cat())
^
error: Cannot prove that Lion =:= Product with Animal with java.io.Serializable.
Now compiler still has to try to generate the least upper bound of given arguments, however it also has to satisfy the additional requirement of being able to generate the witness ev
for two types A
and B
being equal. (Note the witness ev
will be automagically instantiated by the compiler if it is possible.)
Once we have the witness ev
we can freely move between types A
and B
via its apply
method, for example, consider
scala> type A = Lion
type A
scala> type B = Lion
type B
scala> val a: A = Lion()
val a: A = Lion(...)
scala> val ev: =:=[A, B] = implicitly[A =:= B]
val ev: A =:= B = generalized constraint
scala> ev.apply(a)
val res44: B = Lion(...)
Note how ev.apply(a)
types to B
. The reason we can apply =:=
in this way is because it is actually a function
scala> implicitly[(A =:= B) <:< Function1[A, B]]
val res43: A =:= B <:< A => B = generalized constraint
so the implicit parameter list
(implicit ev: A =:= B)
is in effect specifying an implicit conversion function
(implicit ev: A => B)
so now compiler is able to inject automatically implicit conversion wherever it is needed so the following
def f[A <: B, B](a: A, b: B)(implicit ev: A =:= B): B = {
val x: B = a
x
}
is automatically expanded to
def f[A <: B, B](a: A, b: B)(implicit ev: A =:= B): B = {
val x: B = ev.apply(a)
x
}
In summary, just like type bounds, the generalised type constraints are an additional way of asking the compiler to do further checks at compile-time on our codebase.
Herd[Cat](...)
etc. ? – bobah