That is because the type parameters are covariant (source):
abstract class Either[+A, +B]
The simplest way to explain variances is with a list:
abstract class MList[A]
case class Cons[A](head: A, tail: MList[A]) extends MList[A]
object MNil extends MList[Nothing]
If we try to instantiate such a list it will not work:
scala> Cons(1, MNil)
<console>:12: error: type mismatch;
found : MNil.type
required: MList[Int]
Note: Nothing <: Int (and MNil.type <: MList[Nothing]), but class MList is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
Cons(1, MNil)
^
As the error message says, the problem is that our type A
is invariant, this means for a type B
that is a subtype of type A
(B <: A
) it is not valid to say List[B] <: List[A]
.
We can change that by declaring A
covariant:
abstract class MList[+A]
// rest as before
scala> Cons(1, MNil)
res3: Cons[Int] = Cons(1,MNil$@5ee988c6)
Covariance means that the subtype relationship of types is forwarded to the outer type, in our example it means that MList[Nothing] <: MList[Int]
because Nothing
is Scalas bottom type and thus a subtype of every possible type.
But now, we have a problem. We can't add methods to our MList
which expect a parameter of type A
:
scala> :paste
// Entering paste mode (ctrl-D to finish)
abstract class MList[+A] {
def Cons(b: A) = new Cons(b, this)
}
case class Cons[A](head: A, tail: MList[A]) extends MList[A]
object MNil extends MList[Nothing]
// Exiting paste mode, now interpreting.
<console>:11: error: covariant type A occurs in invariant position in type (b: A)Cons[A] of method Cons
def Cons(b: A) = new Cons(b, this)
^
<console>:11: error: covariant type A occurs in contravariant position in type A of value b
def Cons(b: A) = new Cons(b, this)
^
To explain why the compiler rejects this code it is necessary to go a bit deeper into the system. We assume that List[B] <: List[A]
is always true when B <: A
. Then the following code would compile:
val xs: Array[Any] = Array[Int](18)
xs(0) = "hello"
Because Int <: Any
, it is also true that Array[Int] <: Array[Any]
. And because String <: Any
is true too the compiler could translate this code without any problems, but at runtime it would fail because we can't store a String
inside of an Array[Int]
(there is no type erasure for arrays on the JVM). Thus, in Scala the assignment of a Array[Int]
to an Array[Any]
is not valid, the code is rejected. Nevertheless, for List
we don't get an error:
scala> val xs: List[Any] = List[Int](18)
xs: List[Any] = List(18)
The reason for the different behavior is because List
is covariant while Array
is not (it is invariant). But why the difference? That's because of the immutable nature of List
, the elements of a List
can not be changed. But for Array
we can change the elements - in other words with variances the programmer can give the compiler the information that elements are mutable or not and that it should care that no one does silly things with our code.
We go back to our MList
that needs to be fixed. Because it is immutable we can safely declare it as covariant. In fact we need to do it because otherwise we can't use MNil
. It is not possible to give type parameters to an object, thus in order to avoid type problems later, we need to extend MList
with the lowest possible type, which is Nothing
. To fix the problem we need to set a lower bound:
abstract class MList[+A] {
def Cons[B >: A](b: B) = new Cons(b, this)
}
case class Cons[A](head: A, tail: MList[A]) extends MList[A]
object MNil extends MList[Nothing]
scala> MNil Cons 2 Cons 1
res10: Cons[Int] = Cons(1,Cons(2,MNil$@2e6ef76f))
B >: A
is our lower bound and means A
is a subtype of B
or B
is a supertype of A
. To get a MList[Int]
when we have a MList[Nothing]
(which is MNil
) we need such a lower bound because Int >: Nothing
and we have MList[Nothing].Cons[Int].Cons[Int]
.
So, that is the reason why joinLeft
needs such lower bounds too. In fact, it is true that for joinLeft
only the second type bound B1 >: B
is necessary, the other one is not needed because <:<
already is contravariant in its first type parameter. This means the lower bound on A
doesn't have any effect (side note: I opened a pull request that deletes the lower bound on A
but the change is rejected because it breaks backwards source compatibility and no workaround could be found.)