1

Following the nice answer to this question "What's the deal with the Either cruft?", for Either[+A, +B], joinLeft is defined as

joinLeft [A1 >: A, B1 >: B, C] (implicit ev: <:<[A1, Either[C, B1]]):
         Either[C, B1]

@Didier said "A1 and B1 are technically necessary, but not critical to the understanding..."; however, I'm curious about why we can't just have

joinLeft[C](implicit ev: <:<[A, Either[C, B]): Either[C, B]

EDITED
I tried to applied the concept of variance to joinLeft. Firstly, the source of joinLeft:

abstract class Either[+A, +B]

def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]):
    Either[C, B1] = this match {
    case Left(a)  => a
    case Right(b) => Right(b)
}

I might be wrong but the 2ed case seems easier to me:
Right(b) would need to satisfy Either[C, B1] >: Either[C, B]. Because Either is covariant, so B1 >: B.

But there's a problem: why we have joinLeft of type Either[C, B1] rather than just Either[C, B]? Is it because it's less constrained?

implicit ev: (<:<[A1, Either[C, B1]]) can only be able to convert from A's supertype A1 to A1's supertype Either[C, B1]. I think it's used for a in the 1st case. But I feel that A1 isn't necessary. Can we define joinLeft as such:

def joinLeft[B1 >: B, C](implicit ev: A <:< Either[C, B1])

? My analysis might be nonsense. Please feel free to correct me.


EDITED
The answer to my another question type constraints and reifications regarding to joinLeft of Either is relevant to this one.


EDITED

Thanks very much to @sschaef for keeping track on this. The relevant discussion can be found here:


Community
  • 1
  • 1
cfchou
  • 1,239
  • 1
  • 11
  • 25

1 Answers1

3

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.)

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
  • Thank you for the thorough explanation. But I'm a bit puzzled by the last example. `Cons(1, Cons(2, MNil))` would work without MList's Cons. Did you mean that if I define a MList.mcons and like to do something like `Cons(1, MNil).mcons(2)`? – cfchou Apr 12 '13 at 19:24
  • I tried to applied the concept of variance to joinLeft in my question and edited it. But I still couldn't make out. Please feel free to comment on my update. – cfchou Apr 12 '13 at 20:57
  • @cfchou: I can't think of a use case for the lower bound on `A` at the moment, maybe you are right and it is useless. – kiritsuku Apr 12 '13 at 21:30
  • @cfchou: Ok, after thinking about this again, the lower bound on `A` seems unnecessary, because `<:<` is already contravariant in its first type argument. But I may be wrong too... – kiritsuku Apr 12 '13 at 22:18
  • @cfchou: I think now I can give a final answer: `A1` is unnecessary for `joinLeft` (see my last paragraph). – kiritsuku May 02 '13 at 15:34