Reputation: 1035
I have been reviewing Scala knowledge and have been going circles about the variance/lower bound.
In the 'functional programming in scala' book, the Either type, it has below signature/exercise (Implement versions of flatMap, orElse on Either that operate on the Right value).
sealed trait Either[+E,+A] {
def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = ???
def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] = ???
}
and the note of the book says
when mapping over the right side, we must promote the left type parameter to some supertype, to satisfy the +E variance annotation. similarly for 'orElse'
My question are:
B >: A
in the flatMap
function? we do not need to satisfy +A
?orElse
signature requires B >: A
?I understand method parameters count as contravariant positions, so we could not possibly have A
or E
in the method's parameter. i.e. the 'return type' of the f
or b
could not have E
or A
in it.
Maybe I am missing something, in relation to the fundamental knowledge of subtyping/lower bound/function as parameter.
Please help me to understand it with maybe some concrete examples.
p.s. Most articles, about variance or upper/lower bound, I found have only 1 type parameter in the class/trait.
Upvotes: 2
Views: 238
Reputation: 51658
If Either
were invariant, signatures would be
sealed trait Either[E,A] {
def flatMap[B](f: A => Either[E, B]): Either[E, B] = ???
def orElse(b: => Either[E, A]): Either[E, A] = ???
}
There is no connection between A
and B
here.
Now if we make Either
covariant with respect to E
we have to add EE >: E
sealed trait Either[+E,A] {
def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = ???
def orElse[EE >: E](b: => Either[EE, A]): Either[EE, A] = ???
}
Otherwise if we make Either
covariant with respect to A
we have to add AA >: A
sealed trait Either[E,+A] {
def flatMap[B](f: A => Either[E, B]): Either[E, B] = ???
def orElse[AA >: A](b: => Either[E, AA]): Either[E, AA] = ???
}
Just AA
is denoted as B
.
In actual case Either
is covariant with respect to both type parameters so this is the combination of above.
I guess now it's clear that B
in flatMap
and B
in orElse
are different.
Upvotes: 0
Reputation: 149528
why do not we have to say
B >: A
in the flatMap function? we do not need to satisfy+A
?
flatMap
does not put any constraint on the type produced by f: A => Either[EE, B]
. This means, for example, that we can have a Either[Throwable, String]
and use flatMap
to convert it to an Either[Throwable, Int]
. Note that the only relationship between String
and Int
is through Any
.
why does orElse signature requires B >: A
When we say: "Give me the left hand side, or else give me the right hand side" we usually want both types to align such that our "fallback", via orElse
, will provide a meaningful fallback.
For example, let's use the above example and say we want to take an Either[Throwable, String]
and convert it to a Either[Throwable, Int]
using flatMap
:
val either: Either[Throwable, String] = Right("42")
val res: Either[Throwable, Int] = either.flatMap(str => Try(str.toInt).toEither)
This will work when our String
is 42, but if it's not a valid Int
, we'll get a Left[Throwable]
back. Now let's decide that in case parsing fails, we always want to return -1 as a default value (of course there are better ways to model this, but stick with me). We can leverage orElse
for this:
val either: Either[Throwable, String] = Right("42")
val res: Either[Throwable, Int] = either.flatMap(str => Try(str.toInt).toEither).orElse(Right(-1))
This way, the relationship between the LHS and the RHS is preserved, and we receive a sensible value as the result. If B
was not constrained to A
at all, we would usually get a supertype far up in the type hierarchy, such as AnyRef
or Any
.
One additional thing about the EE >: E
constraint. Since E
is covariant, if we tried to use it as a type parameter for the flatMap
function:
sealed trait Either[+E, +A] {
def flatMap[B](f: A => Either[E, B]): Either[E, B] = ???
}
The compiler would yell at us:
Error:(7, 20) covariant type E occurs in contravariant position in type A => Either[E,B] of value f def flatMap[B](f: A => Either[E, B]): Either[E, B] = ???
That's because covariant types cannot "go in" to the method, they can only be used in the return type, contrary to contravariant type parameters which "go in", but cannot be used in the result type.
Upvotes: 4