DEV Community

Hannes Halenka
Hannes Halenka

Posted on

Why I don't like Scala's "Either"

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:

  1. The fact that the function returns a Boolean does not encourage to use it in a purely functional manner.
  2. 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?

  1. Option does not work since it wraps only one type and this type should represent a valid result.
  2. Either can wrap two types but the naming of its concrete implementations Left and Right suffer from a lack of semantics.
  3. 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)

Collapse
 
mikegirkin profile image
Mikhail Girkin • Edited

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 return Result[AA, B] to stay consistent with usual definition of functor and map function. If you would like to be able to convert to Option, the conventional name would be .toOption or .asOption.