I’m creating this thread to replace https://github.com/scala/bug/issues/10895, where @xuwei-k proposed changing Either's subclasses. As he said it would remove the need for some asInstanceOf calls such as:
def map[B1](f: B => B1): Either[A, B1] = this match {
case Right(b) => Right(f(b))
- case _ => this.asInstanceOf[Either[A, B1]]
+ case l@Left(_) => l
}
What do you think? Are there any advantages from the current definition?
Right case class only captures the right-side of Either[+A, +B] disjunction. So it makes sense to me that the type parameter that it holds are limited only to B. The map shows the practical problem with carrying both side’s type information needlessly.
In the example, this is typed to Either[A, B]. Even in the case for l@Left(_), the type is Either[A, B]. The right-biased map would then transform the right-side, and thus change the return type to Either[A, B1]. So l@Left(_) gets a type error saying found Either[A, B] when Either[A, B1] is expected.
scala.Nothing
We would eventually want to treat Left(...) and Right(...) as Either[+A, +B], so we need a placeholder type. Using the bottom type Nothing gives us the maximum flexibility since:
Nothing is a subtype of ever other type, and
Either[+A, +B] is covariant on both sides,
so a value of Either[Nothing, B] can reside on an arbitrary Either[?, B]. Woot.
We have precedence for using the bottom type this way:
case object Nil extends List[Nothing]
....
case object None extends Option[Nothing]
The “empty” values of List and Option are both parameterized with Nothing.
Con
If in the source code you have Right[Foo, Bar], this change breaks the source compatibility.
I think it’s a minor problem since you can just use Either[Nothing, Bar] or Either[Foo, Bar] as the type.
If we are willing to accept this suggestion, I’d like to propose an additional small enhancement.
object Left {
/** Creates the left side of the disjoint union.
*/
def apply[A](value: A): Either[A, Nothing] = new Left(value)
}
This explicitly defines Left.apply(...) instead of using the normal factor method case class gives us out of the box. This hopefully helps the transition since the one-parameter version of Left become a bit more hidden.
The added benefit is that either you use Left(...) or Right(...), the type becomes Either[A, B] sort of similar to Dotty’s enums. This is convenient if we want to use Either with typeclasses.
I’d like to see Left.apply take 2 type params to avoid the need for type ascription in various circumstances. Left would still extend Either[L, Nothing].
object Left {
/** Creates the left side of the disjoint union.
*/
def apply[L, R](value: L): Either[L, R] = new Left(value)
}
But it’s not consistent with Nil/:: and None/Some.
I would be OK with methods Either.left[L, R]() and Either.right[L, R] just like they are in cats. But if Left.apply is supposed to return an Either, then Nil should return a List, but that opens up another can of worms.
AFAIK scalaz tries very hard to pretend Scala doesn’t have subtyping, so that’s not surprising It’s a valid design space, but one that doesn’t match the design of the standard library.
For the next generation of collection library, which will stay with us into the Scala 3 transition, I think Nil/:: returning List and None/Some returning Option makes perfect sense. Dotty enum already uses Option as an example:
enum Option[+T] {
case Some(x: T) extends Option[T]
case None extends Option[Nothing]
}
Yes, having it be Some[Int] tells the Scala compiler that the unapplication cannot fail, which allows us to retain exhaustive checking of pattern matching expressions.
If you want Option.Some[Int] you have to use new Option.Some(mc.x) instead.
Also, I’m really really surprised by this change in Dotty. I’m not sure I’d describe it as a “a subtle difference with respect to normal case classes”.