The problem
Let's say we are assigned to develop a function that validates usernames. For the sake of the example, the function only checks whether the provided String
value is between four and ten characters long.
A first draft of the function could be something like that:
def validateUsername(value: String): Boolean = {
val length = value match {
case s: String => s.length
case _ => 0
}
length >= 4 && length <= 10
}
This implementation returns true
for all valid usernames, but has two unfortunate shortcomings:
- The fact that the function returns a
Boolean
does not encourage to use it in a purely functional manner. - If a username is invalid, the caller does not get any information about why.
The first problem can be easily fixed by slightly modifying implementation:
def validateUsername(value: String): Option[String] = value match {
case s: String if (s.length >= 4 && s.length <= 10) => Some(value)
case _ => None
}
To be able to remedy the second shortcoming, we need to elaborate in more detail.
That's why Option
is not a good option ...
While Option
allows the use of validateUsername
in a functional style, it does not provide information about why the validation failed.
What we need is a way to return the validated username, or an error indicating why the validation failed. Luckily, Scala offers several solutions to this problem - let's take a closer look at some of them.
... and Either
neither.
With scala.util.Either
the Scala Standard Library offers a container that wraps two (different) types.
Let's modify our example so that it uses Either
as the return type:
def validateUsername(value: String): Either[String, String] = {
val min = 4 // minimum legnth for username
val max = 10 // maximum lenght for username
val length = value match {
case s: String => s.length
case _ => 0
}
if (length >= min && length <= max) {
Right(value)
} else if (length < min) {
Left("'value' is too short.")
} else {
Left("'value' is too long.")
}
}
Now, let's use validateUsername
in a simple example:
validateUsername("user") match {
case Right(u) => println(s"A valid username: '$u'");
case Left(r) => println(s"Invalid username; reason: '$r'")
}
This does not look too bad, but as the title suggests: I strongly dislike Scala's Either
, and I am going to explain to you why.
To fully understand what I don't like about Either let's have a brief look at its implementation.
Much like Option
, Either
is a sealed abstract class with two concrete implementations (Left
and Right
), but with the important difference of taking two type parameters instead of one. I don't have so much a problem with the implementations of the derived classes as with their naming.
To me, there is not much semantic to Left
and Right
. Of course, the names refer to the position of the respective type parameters in the declaration, but that's about all. One does not need to suffer from dyslexia to get confused. Which type indicates that the function returned without an error? Is it Left
because it is declared first or is it Right
because it spells like "right"?
How nice would it be to have result types that indicate doubtlessly whether a function returned OK or with an error? Let's see what else the Scala Standard Library has to offer to reflect our desires.
Let's Try
something else
scala.util.Try
was introduced in Scala 2.10 and is meant to perform Exception prone operations without the need to care about explicit exception-handling.
import scala.util.{Try, Success, Failure}
Try(1 / 0) match {
case Success(s) => println(s"Result: $s")
case Failure(x) => println(s"Exception: $x")
}
Try
works fine with exceptions but can we use it as a return type for our validateUsername
function? We can but this would make the implementation somehow awkward and less concise:
1. Throwing an exception if the validation fails
import scala.util.{Try, Success, Failure}
def validateUsername(value: String): Try[String] = {
val min = 4 // minimum legnth for username
val max = 10 // maximum lenght for username
val length = value match {
case s: String => s.length
case _ => 0
}
if (length < min) {
Failure(new Exception("'value' is too short."))
} else if (length > max) {
Failure(new Exception("'value' is too long."))
} else {
Success(value)
}
}
validateUsername("username") match {
case Success(username) => println(s"Valid username: $username")
case Failure(ex) => println(s"Invalid: '${ex.getMessage}'.")
}
Here we deliberately throw an exception if the validation of the username fails. This is not a good idea since exceptions should only be thrown in exceptional situations and should never be an expected and valid "result" of the invocation of a function.
2. Try
as result type
import scala.util.{Try, Success, Failure}
def validateUsername(value: String): Try[String] = {
val min = 4 // minimum legnth for username
val max = 10 // maximum lenght for username
val length = value match {
case s: String => s.length
case _ => 0
}
if (length < min) {
Failure(new Exception("'value' is too short."))
} else if (length > max) {
Failure(new Exception("'value' is too long."))
} else {
Success(value)
}
}
validateUsername("username") match {
case Success(username) => println(s"Valid username: $username")
case Failure(ex) => println(s"Invalid: '${ex.getMessage}'.")
}
In the second example, we do not throw but create exception instances because Failure
requires an exception object. Creating new instances of an exception without throwing them immediately is only acceptable in rare cases and if we come across situations where instances of exceptions are created but not thrown we should be alarmed.
What have we learned so far?
-
Option
does not work since it wraps only one type and this type should represent a valid result. -
Either
can wrap two types but the naming of its concrete implementationsLeft
andRight
suffer from a lack of semantics. - Throwing exceptions in the case of a failed validation or using
Try
as a result type is not a good idea either.
So does that mean we are out of luck? As for Scala 2.13, the Standard Library does not provide an appropriate type to solve our problem. If we stopped here I actually would suggest using Either
since it is the smallest evil there.
A Rust-like Result
type
But wait, I am not stopping just there. Scala is an ever-evolving project and so I propose a Rust-like Result
type:
sealed abstract class Result[+A, +B]
final case class Success[+A, +B](success: A) extends Result[A, B]
final case class Failure[+A, +B](failure: B) extends Result[A, B]
Already this simple, stub-like implementation would make our example more concise:
def validateUsername(value: String): Result[String, String] = {
val min = 4 // minimum legnth for username
val max = 10 // maximum lenght for username
val length = value match {
case s: String => s.length
case _ => 0
}
if (length >= min && length <= max) {
Success(value)
} else if (length < min) {
Failure("'value' is too short.")
} else {
Failure("'value' is too long.")
}
}
validateUsername("usr") match {
case Success(u) => println(s"A valid username: '$u'");
case Failure(r) => println(s"Invalid username; reason: '$r'")
}
If we want to use Result
in a more sophisticated way, we can add a map
function:
sealed abstract class Result[+A, +B] {
def map[AA](f: A => AA): Option[AA] = this match {
case Success(a) => Some(f(a))
case Failure(_) => None
}
}
This would allow us to only take care of the happy path:
validateUsername("username").map(x => x).foreach(createUserAccount(_));
In my opinion, there is a great potential to a Result
type in Scala which would make error handling more concise and easier to understand.
Top comments (1)
I agree with the main point of the article -
Either
is not very explicit when expressing potential errors, and current usage is based solely on convention.But I don't see any benefit of adding one more type that would be exactly the same as
Either
, except for naming.Minor nitpick: for
Result
type you proposed,map
should returnResult[AA, B]
to stay consistent with usual definition of functor andmap
function. If you would like to be able to convert toOption
, the conventional name would be.toOption
or.asOption
.