The Either class seems useful and the ways of using it are pretty obvious. But then I look at the API documentation and I'm baffled:
def joinLeft [A1 >: A, B1 >: B, C] (implicit ev: <:<[A1, Either[C, B1]]):
Either[C, B1]
Joins an Either through Left.
def joinRight [A1 >: A, B1 >: B, C] (implicit ev: <:<[B1, Either[A1, C]]):
Either[A1, C]
Joins an Either through Right.
def left : LeftProjection[A, B]
Projects this Either as a Left.
def right : RightProjection[A, B]
Projects this Either as a Right.
What do I do with a projection and how do I even invoke the joins?
Google just points me to the API documentation.
This might just be a case of "paying no attention to the man behind the curtain", but I don't think so. I think this is important.
left
and right
are the important ones. Either
is useful without projections (mostly you do pattern matching), but projections are quite worthy of attention, as they give a much richer API. You will use joins much less.
Either
is often used to mean "a proper value or an error". In this respect, it is like an extended Option
. When there is no data, instead of None
, you have an error.
Option
has a rich API. The same can be made available on Either
, provided we know, in Either, which one is the result and which one is the error.
left
and right
projection says just that. It is the Either
, plus the added knowledge that the value is respectively at left or at right, and the other one is the error.
For instance, in Option
, you can map, so opt.map(f)
returns an Option
with f
applied to the value of opt
if it has a one, and still None
if opt
was None
. On a left projection, it will apply f
on the value at left if it is a Left
, and leave it unchanged if it is a Right
. Observe the signatures:
LeftProjection[A,B]
, map[C](f: A => C): Either[C,B]
RightProjection[A,B]
, map[C](f: B => C): Either[A,C]
.left
and right
are simply the way to say which side is considered the value when you want to use one of the usual API routines.
Alternatives could have been:
map
for instance), do a swap
before and after. for
comprehensions (flatMap
in fact, but the for notation is quite convenient) Either
is an alternative to (checked) exceptions. Now the joins. Left and Right means the same thing as for the projections, and they are closely related to flatMap
. Consider joinLeft
. The signature may be puzzling:
joinLeft [A1 >: A, B1 >: B, C] (implicit ev: <:<[A1, Either[C, B1]]):
Either[C, B1]
A1
and B1
are technically necessary, but not critical to the understanding, let's simplify
joinLeft[C](implicit ev: <:<[A, Either[C, B])
What the implicit means is that the method can only be called if A
is an Either[C,B]
. The method is not available on an Either[A,B]
in general, but only on an Either[Either[C,B], B]
. As with left projection, we consider that the value is at left (that would be right for joinRight
). What the join does is flatten this (think flatMap
). When one join, one does not care whether the error (B) is inside or outside, we just want Either[C,B]. So Left(Left(c)) yields Left(c), both Left(Right(b)) and Right(b) yield Right(b). The relation with flatMap is as follows:
joinLeft(e) = e.left.flatMap(identity)
e.left.flatMap(f) = e.left.map(f).joinLeft
The Option
equivalent would work on an Option[Option[A]]
, Some(Some(x))
would yield Some(x)
both Some(None)
and None
would yield None
. It can be written o.flatMap(identity). Note that Option[A]
is isomorphic to Either[A,Unit]
(if you use left projections and joins) and also to Either[Unit, A]
(using right projections).
Ignoring the joins for now, projections are a mechanism allowing you to use use an Either
as a monad. Think of it as extracting either the left or right side into an Option
, but without losing the other side
As always, this probably makes more sense with an example. So imagine you have an Either[Exception, Int]
and want to convert the Exception
to a String
(if present)
val result = opReturningEither
val better = result.left map {_.getMessage}
This will map over the left side of result, giving you an Either[String,Int]
joinLeft
and joinRight
enable you to "flatten" a nested Either
:
scala> val e: Either[Either[String, Int], Int] = Left(Left("foo"))
e: Either[Either[String,Int],Int] = Left(Left(foo))
scala> e.joinLeft
res2: Either[String,Int] = Left(foo)
Edit: My answer to this question shows one example of how you can use the projections, in this case to fold together a sequence of Either
s without pattern matching or calling isLeft
or isRight
. If you're familiar with how to use Option
without matching or calling isDefined
, it's analagous.
While curiously looking at the current source of Either, I saw that joinLeft
and joinRight
are implemented with pattern matching. However, I stumbled across this older version of the source and saw that it used to implement the join methods using projections:
def joinLeft[A, B](es: Either[Either[A, B], B]) =
es.left.flatMap(x => x)
My suggestion is add the following to your utility package:
implicit class EitherRichClass[A, B](thisEither: Either[A, B])
{
def map[C](f: B => C): Either[A, C] = thisEither match
{
case Left(l) => Left[A, C](l)
case Right(r) => Right[A, C](f(r))
}
def flatMap[C](f: B => Either[A, C]): Either[A, C] = thisEither match
{
case Left(l) => Left[A, C](l)
case Right(r) => (f(r))
}
}
In my experience the only useful provided method is fold. You don't really use isLeft or isRight in functional code. joinLeft and joinRight might be useful as flatten functions as explained by Dider Dupont but, I haven't had occasion to use them that way. The above is using Either as right biased, which I suspect is how most people use them. Its like an Option with an error value instead of None.
Here's some of my own code. Apologies its not polished code but its an example of using Either in a for comprehension. Adding the map and flatMap methods to Either allows us to use the special syntax in for comprehensions. Its parsing HTTP headers, either returning an Http and Html error page response or a parsed custom HTTP Request object. Without the use of the for comprehension the code would be very difficult to comprehend.
object getReq
{
def LeftError[B](str: String) = Left[HResponse, B](HttpError(str))
def apply(line1: String, in: java.io.BufferedReader): Either[HResponse, HttpReq] =
{
def loop(acc: Seq[(String, String)]): Either[HResponse, Seq[(String, String)]] =
{
val ln = in.readLine
if (ln == "")
Right(acc)
else
ln.splitOut(':', s => LeftError("400 Bad Syntax in Header Field"), (a, b) => loop(acc :+ Tuple2(a.toLowerCase, b)))
}
val words: Seq[String] = line1.lowerWords
for
{
a3 <- words match
{
case Seq("get", b, c) => Right[HResponse, (ReqType.Value, String, String)]((ReqType.HGet, b, c))
case Seq("post", b, c) => Right[HResponse, (ReqType.Value, String, String)]((ReqType.HPost, b, c))
case Seq(methodName, b, c) => LeftError("405" -- methodName -- "method not Allowed")
case _ => LeftError("400 Bad Request: Bad Syntax in Status Line")
}
val (reqType, target, version) = a3
fields <- loop(Nil)
val optLen = fields.find(_._1 == "content-length")
pair <- optLen match
{
case None => Right((0, fields))
case Some(("content-length", second)) => second.filterNot(_.isWhitespace) match
{
case s if s.forall(_.isDigit) => Right((s.toInt, fields.filterNot(_._1 == "content-length")))
case s => LeftError("400 Bad Request: Bad Content-Length SyntaxLine")
}
}
val (bodyLen, otherHeaderPairs) = pair
val otherHeaderFields = otherHeaderPairs.map(pair => HeaderField(pair._1, pair._2))
val body = if (bodyLen > 0) (for (i <- 1 to bodyLen) yield in.read.toChar).mkString else ""
}
yield (HttpReq(reqType, target, version, otherHeaderFields, bodyLen, body))
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With