According to the documentation:
A common use of Either is as an alternative to Option for dealing with possible missing values.
Why would you use one over the other?
The nice thing about Either
is that you can keep track of the reason something is missing. For example, if you were working with Options
, you might be in a situation like this:
val xOpt = Option(1)
val yOpt = Option(2)
val zOpt = None
val tupled = for {
x <- xOpt
y <- yOpt
z <- zOpt
} yield (x, y, z)
Now, if tupled
is None
, we don't really know why! If this is an important detail for the rest of the behavior, using Either
can help:
val tupled = for {
x <- xOpt.toRight("x is missing").right
y <- yOpt.toRight("y is missing").right
z <- zOpt.toRight("z is missing").right
} yield (x, y, z)
This will return either Left(msg)
where the message is the first missing value's corresponding message, or Right(value)
for the tupled value. It is conventional to keep use Left
for failures and Right
for successes.
Of course, you can also use Either
more broadly, not only in situations with missing or exceptional values. There are other situations where Either
can help express the semantics of a simple union type.
The third common idiom to use for exceptional values is the Try
monad:
val xTry = Try("1".toInt)
val yTry = Try("2".toInt)
val zTry = Try("asdf".toInt)
val tupled = for {
x <- xTry
y <- yTry
z <- zTry
} yield (x, y, z)
Try[A]
is isomorphic to Either[Throwable, A]
. In other words you can treat a Try
as an Either
with a left type of Throwable
, and you can treat any Either
that has a left type of Throwable
as a Try
. Also Option[A]
is homomorphic to Try[A]
. So you can treat an Option
as a Try
that ignores errors. Therefore you can also transitively think it as an Either
. In fact, the standard library supports some of these transformations:
//Either to Option
Left[Int, String](1).left.toOption //Some(1)
Right[Int, String]("foo").left.toOption //None
//Try to Option
Try("1".toInt).toOption //Some(1)
Try("foo".toInt).toOption //None
//Option to Either
Some(1).toRight("foo") //Right[String, Int](1)
(None: Option[Int]).toRight("foo") //Left[String, Int]("foo")
Since Scala 2.12, the standard library does include the conversions from Either
to Try
, from Try
to Either
, but not from Option
to Try
. For earlier versions, it is pretty simple to enrich Option
, Try
, and Either
as needed:
object OptionTryEitherConversions {
implicit class EitherToTry[L <: Throwable, R](val e: Either[L, R]) extends AnyVal {
def toTry: Try[R] = e.fold(Failure(_), Success(_))
}
implicit class TryToEither[T](val t: Try[T]) extends AnyVal {
def toEither: Either[Throwable, T] =
t.map(Right(_)).recover(Left(_)).get
}
implicit class OptionToTry[T](val o: Option[T]) extends AnyVal {
def toTry(throwable: Throwable): Try[T] =
o.map(Success(_)).getOrElse(Failure(throwable))
}
}
This would allow you to do:
import OptionTryEitherConversions._
//Try to Either
Try(1).toEither //Either[Throwable, Int] = Right(1)
Try("foo".toInt).toEither //Either[Throwable, Int] = Left(java.lang.NumberFormatException)
//Either to Try
Right[Throwable, Int](1).toTry //Success(1)
Left[Throwable, Int](new Exception).toTry //Failure(java.lang.Exception)
//Option to Try
Some(1).toTry(new Exception) //Success(1)
(None: Option[Int]).toTry(new Exception) //Failure(java.lang.Exception)
Either
can be seen as a generalization of Option
.
If you fix the first value of Either
, for example by setting it to Unit
, you would get something that behaves in essentially the same way as Option
:
Option[X] := Either[Unit, X]
In this case, Left[Unit, X]
would correspond to None
, and Right[Unit, X]
would correspond to Some[X]
.
For Option[X]
, None
would signal some kind of failure to obtain a value of type X
, and Some[X]
would signal success.
For Either[Unit, X]
, instance of type Left[Unit, X]
would represent failure, and Right[Unit, X]
would represent success.
However, you can use the first component of Either
to store more detailed information about why something failed, or some additional information that helps you to recover from an error. Option
gives you just a None
, which is not very useful.
But Either[F,X]
could return either a success value Right[F, X]
, which is essentially just a wrapper for X
, or a detailed description of failure in Left[F, X]
, with a value of type F
representing the failure.
This allows you to define more sophisticated recovery strategies.
For example, take a look at Form.scala
of the Play!-Framework.
They use Either
all over the place, because they want to either respond to user's form submission, or to send back a partially filled form, annotated with helpful error messages. The alternative to this would be to work with Option[TypeOfFormContent]
, which would evaluate to None
if some of the form fields contained invalid input. This in turn would mean that the user gets something like "Bad Request. Please fill the whole form again." as response, which would be utterly annoying. Therefore, Either
is used instead of Option
, because it can actually keep track of what exactly went wrong with the form submission.
The disadvantage of Either
is that it is not a monad: in order to work with it effectively, you always have to pass two different callbacks for the two different cases. This can result in a "callback-hell". Therefore, one should think carefully whether an exact description of the failure is that valuable. In the case of a failed form submission, a detailed description of the failure is valuable, because one does not want to force the user to retype everything again. In other cases, Option
might be more appropriate, because one does not want to force the programmers to deal with unnecessary detailed descriptions of unrecoverable errors.
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