Proof By Example

Programming blog by Mark Feeney

Combining Option and Either in Scala

My last post on nested if-let in Clojure exists in part because I have worked with Scala for many years, and in Scala it’s there’s a very handy syntax for the “only act when a bunch of optional things are defined” situation.

val x: Option[Int] = ...
val y: Option[Int] = ...
val z: Option[Int] = ...

val result =
  for {
    a <- x
    b <- y
    c <- z
  } yield a + b +c

Here, result will be None unless all of x, y, z are defined. This is a common idiom in Scala, and a nice one, IMO.

But what if, like in the other post, we want to know which of the optional things is undefined when something goes wrong? (Constraint: without going outside the standard library, i.e. no scalaz.)

Scala has an Either type which seems appropriate. I assumed I could “convert” my Options to Eithers and use the for comprehension syntax as usual. It generally works that way, but there’s a twist that took me a bunch of time to figure out. Here’s where I landed.

val x: Option[Int] = Some(1)
val y: Option[Int] = Some(2)
val z: Option[Int] = None

// Just look away from that return type.
def toRight[Ok, Err](x: Option[Ok], orElse: => Err): Either.RightProjection[Err, Ok] = {
  val either =
    x match {
      case Some(x) => Right(x)
      case None => Left(orElse)
    }
  either.right // WTF?  See below.
}

val result: Either[String, Int] =
  for {
    a <- toRight(x, { println("side effect!"); "x was None" })
    b <- toRight(y, "y was None")
    c <- toRight(z, "z was None")
  } yield {
    a + b + c
  }

result match {
  case Right(x) => println("Result was %d".format(x))
  case Left(msg) => println("Error, msg was '%s'".format(msg))
}

// prints "Error, msg was 'z was None'" in this case.

Confusingly to me, after converting the Option to a Right or Left you have to project it to a RightProjection (chosing Right vs Left is simply convention) since Either itself doesn’t have a flatMap method. I found this especially bizarre because you are explicitly calling .right on the Left you just created. Types! Anyway, I am not alone in finding this weird, apparently. Gory details and one fix proposal here.

In fairness, this bit of localized weirdness results in fairly concise syntax in the for comprehension itself.