FEH: Either (Scala 3 Video)
Introduction: Using Either instead of Option
The last error-handling data type that’s built into Scala is named Either
. Like Option
and Try
, Either
has two sub-types named Left
and Right
, and they match up like this:
Option <=> Try <=> Either
Some <=> Success <=> Right
None <=> Failure <=> Left
That’s a slight over-simplification, because what happens with Either
is that Right
and Left
can contain anything. By convention, Right
holds the success value and Left
holds the error value, but technically you can use those two values however you like.
makeInt with Either
That being said, here’s the makeInt
function written to use Either
. In this case I’ll return a String
in the Left
position:
// no 'import' statements are needed for this
/**
* Left is a String.
*/
def makeInt(s: String): Either[String, Int] =
try
Right(s.toInt)
catch
// for this example i convert the exception to a string
case e: NumberFormatException => Left(e.getMessage)
Now when you call makeInt
with good and bad strings, you’ll see these results:
// success case
makeInt("1") // Right(1)
// failure case
makeInt("one") // Left(For input string: "one")
In that example I return a String
in the Left
position, but I can also return an exception (or any other data type):
/**
* The Left type is now an Exception.
*/
def makeInt(s: String): Either[Exception, Int] =
try
Right(s.toInt)
catch
case e: NumberFormatException => Left(e)
This function’s error value looks like this:
// failure case
makeInt("one") //Left(java.lang.NumberFormatException: For input string: "a")
Handling the result
Common ways to work with the result of an Either
are match
and for
expressions.
The second version of makeInt
returns an exception in the Left
position, and this is how that function works in a match
expression:
makeInt(aString) match
case Right(i) => println(s"Success: i = $i")
case Left(e) => println(s"Failed, exception = $e")
And this is what that function looks like when its used with multiple strings that can be converted to integers in a for
expression:
val maybeSum: Either[Exception, Int] =
for
i <- makeInt("1")
j <- makeInt("2")
k <- makeInt("3")
yield
i + j + k
// result: maybeSum == Right(6)
As shown in the comment, in this situation, maybeSum
has the value Right(6)
. And this code shows what the Unhappy Path looks like when one or more strings can’t be converted to integers:
val maybeSum: Either[Exception, Int] =
for
i <- makeInt("1")
j <- makeInt("Hi mom!")
k <- makeInt("3")
yield
i + j + k
// result:
maybeSum == Left(java.lang.NumberFormatException: For input string: "Hi mom!")
Here, maybeSum
is a Left
, and it contains that NumberFormatException
.
And just like Option
and Try
, whether you get a Right
or Left
back from the for
expression, you can handle its result in a match
expression:
maybeSum match
case Right(i) => println(s"Success: i = $i")
case Left(e) => println(s"Failed: msg = ${e.getMessage}")
Notice that when you use Either
, both Right
and Left
are containers that contain the success value and error value, respectively.
Reading an Either signature
In the second version of the makeInt
function, the return value Either[Exception, Int]
can read as, “If the computation succeeds, I’ll get an Int
back that’s wrapped in the Right
. If it fails, I’ll get the exception in the Left
.” The declaration of the two possible return types makes Either
more flexible than the Option
and Try
types.
Two technical points
There are at least two interesting technical points to know about the Either
data type.
First, as that Scaladoc page notes, Either
represents a value that is one of two possible types, and this is technically known as a disjoint union.
Second, Either
is right-biased. The Scaladoc states, “this means that Right
is assumed to be the default case to operate on.” When you use Either
in match
and for
expressions as I showed, this doesn’t matter. But when you want to work directly with maybeSum
, such as calling a map
method on it, that’s where this is important.
The key is that when you call map
on maybeSum
and it is a Left
, map
returns the Left
value unchanged, as shown in the REPL:
// when maybeSum is a Left:
scala> maybeSum.map(_ * 10)
val res0: Either[Exception, Int] =
Left(java.lang.NumberFormatException: For input string: "Hi mom!")
Conversely, when maybeSum
is a Right
, map
and other methods work as desired:
// when maybeSum is a Right:
scala> maybeSum.map(_ * 10)
val res1: Either[Exception, Int] = Right(60)
Pure functions cannot lie
Once again I’ll reiterate this point about the type signatures of pure functions: they cannot lie. As shown here, the makeInt
function screams loud and clear, “I’ll give you an Int
in the Right
position if everything goes well, but if something goes wrong, you’ll get an Exception
in the Left
”:
def makeInt(s: String): Either[Exception, Int]
----------------------
Summary: Whenever you see Either
, Try
, or Option
as the return type of a function, you know that something can go wrong inside that function.
Update: All of my new videos are now on
LearnScala.dev