According to the documentation:
A common use of Either is as an alternative to Option for dealing with possible missing values.
W
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")
The standard library does not include the conversions from Either
to Try
, from Try
to Either
, or from Option
to Try
. But 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(PartialFunction(Left(_))).get
}
implicit class OptionToTry[T](val o: Option[T]) extends AnyVal {
def toTry(throwable: Throwable): Try[T] = o.map(Right(_)).getOrElse(Left(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)