Why do `Left` and `Right` have two type parameters?
Not sure how relevant this answer really is to Scala, but it certainly is in Haskell which is evidently where Scala's Either
was borrowed from and so that's probably the best historical reason for why Scala did it this way.
Either
is the canonical coproduct, i.e. for any types A
and B
you have
- The type
EitherA,B ≈ A ⊕ B
- Two coprojections
LeftA,B : A -> A⊕B
andRightA,B : B -> A⊕B
- such that for any type
Y
and any functionsfA : A -> Y
andfB : B -> Y
, there exists exactly one functionf : A⊕B -> Y
with the property thatfA = f ∘ LeftA,B
andfB = f ∘ RightA,B
.
To formulate this mathematically, it is quite helpful to have the information which particular Left
you're working with explicit, because else the domains of the morphisms would be all unclear. In Scala this may be unnecessary because of implicit covariant conversion, but not in maths and not in Haskell.
In Haskell it isn't really an issue at all, because type inference will automatically do what's needed:
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /tmp/haskell-stack-ghci/2a3bbd58/ghci-script
Prelude> let right2 = Right 2
Prelude> let left42 = Left 42.0
Prelude> (+) <$> right2 <*> left42
Left 42.0
Unlike, apparently, in Scala, Haskell just leaves the unspecified second argument of left42
as a type variable (unless the monomorphism restriction is enabled), so you can later use it in any context requiring some Either Double R
for any type R
. Of course it's possible to make that explicit too
right2 :: Either a Int
right2 = Right 2
left42 :: Either Double a
left42 = Left 42
main :: IO ()
main = print $ (+) <$> right2 <*> left42
which surely is possible in Scala just as well.
There's no meaningful drawback that I've found to your scheme. For the last eight years or so I've used my own variant of Either
which is exactly as you describe under another name (Ok[+Y, +N]
with Yes[+Y]
and No[+N]
as the alternatives). (Historical note: I started when Either
was not right-biased, and wanted something that was; but then I kept using my version because it was more convenient to have only half the types.)
The only case I've ever found where it matters is when you pattern match out one branch and no longer have access to the type information of the other branch.
def foo[A, B: Typeclass](e: Either[A, B]) =
implicitly[Typeclass[B]].whatever()
// This works
myEither match {
case l: Left[L, R] => foo(l)
case r: Right[L, R] => foo(r)
}
def bar[N, Y: Typeclass](o: Ok[N, Y]) =
implicitly[Typeclass[Y]].whatever()
// This doesn't work
myOk match {
case y: Yes[Y] => bar(y) // This is fine
case n: No[N] => bar(n) // Y == Nothing!
}
However, I never do this. I could just use o
to get the right type. So it doesn't matter! Everything else is easier (like pattern matching and changing one case and not the other...you don't need case Left(l) => Left(l)
which rebuilds the Left
for no reason except to switch the type of the uninhabited branch).
There are other cases (e.g. setting types in advance) that seem like they should be important, but in practice are almost impossible to make matter (e.g. because covariance will find the common supertype anyway, so what you set doesn't constrain anything).
So I think the decision was made before there was enough experience with the two ways to do it, and the wrong choice was made. (It's not a very wrong choice; Either
is still decent.)