Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding deeply nested Option cascades in Scala

Say I have three database access functions foo, bar, and baz that can each return Option[A] where A is some model class, and the calls depend on each other.

I would like to call the functions sequentially and in each case, return an appropriate error message if the value is not found (None).

My current code looks like this:

Input is a URL: /x/:xID/y/:yID/z/:zID

foo(xID) match {
  case None => Left(s"$xID is not a valid id")
  case Some(x) =>
    bar(yID) match {
      case None => Left(s"$yID is not a valid id")
      case Some(y) =>
        baz(zID) match {
          case None => Left(s"$zID is not a valid id")
          case Some(z) => Right(process(x, y, z))
        }
    }
}

As can be seen, the code is badly nested.

If instead, I use a for comprehension, I cannot give specific error messages, because I do not know which step failed:

(for {
  x <- foo(xID)
  y <- bar(yID)
  z <- baz(zID)
} yield {
  Right(process(x, y, z))
}).getOrElse(Left("One of the IDs was invalid, but we do not know which one"))

If I use map and getOrElse, I end up with code almost as nested as the first example.

Is these some better way to structure this to avoid the nesting while allowing specific error messages?

like image 428
Ralph Avatar asked Mar 05 '15 18:03

Ralph


3 Answers

You can get your for loop working by using right projections.

def ckErr[A](id: String, f: String => Option[A]) = (f(id) match {
  case None => Left(s"$id is not a valid id")
  case Some(a) => Right(a)
}).right

for {
  x <- ckErr(xID, foo)
  y <- ckErr(yID, bar)
  z <- ckErr(zID, baz)
} yield process(x,y,z)

This is still a little clumsy, but it has the advantage of being part of the standard library.

Exceptions are another way to go, but they slow things down a lot if the failure cases are common. I'd only use that if failure was truly exceptional.

It's also possible to use non-local returns, but it's kind of awkward for this particular setup. I think right projections of Either are the way to go. If you really like working this way but dislike putting .right all over the place, there are various places you can find a "right-biased Either" which will act like the right projection by default (e.g. ScalaUtils, Scalaz, etc.).

like image 149
Rex Kerr Avatar answered Oct 21 '22 07:10

Rex Kerr


Instead of using an Option I would instead use a Try. That way you have the Monadic composition that you'd like mixed with the ability to retain the error.

def myDBAccess(..args..) =
 thingThatDoesStuff(args) match{
   case Some(x) => Success(x)
   case None => Failure(new IdError(args))
 }

I'm assuming in the above that you don't actually control the functions and can't refactor them to give you a non-Option. If you did, then simply substitute Try.

like image 24
wheaties Avatar answered Oct 21 '22 07:10

wheaties


I know this question was answered some time back, but I wanted to give an alternative to the accepted answer.

Given that, in your example, the three Options are independent, you can treat them as Applicative Functors and use ValidatedNel from Cats to simplify and aggregate the handling of the unhappy path.

Given the code:

  import cats.data.Validated.{invalidNel, valid}

  def checkOption[B, T](t : Option[T])(ifNone : => B) : ValidatedNel[B, T] = t match {
    case None => invalidNel(ifNone)
    case Some(x) => valid(x)

  def processUnwrappedData(a : Int, b : String, c : Boolean) : String = ???

  val o1 : Option[Int] = ???
  val o2 : Option[String] = ???
  val o3 : Option[Boolean] = ???

You can then replicate obtain what you want with:

//import cats.syntax.cartesian._
( 
  checkOption(o1)(s"First option is not None") |@|
  checkOption(o2)(s"Second option is not None") |@|
  checkOption(o3)(s"Third option is not None")
 ) map (processUnwrappedData)

This approach will allow you to aggregate failures, which was not possible in your solution (as using for-comprehensions enforces sequential evaluation). More examples and documentation can be found here and here.

Finally this solution uses Cats Validated but could easily be translated to Scalaz Validation

like image 20
mdm Avatar answered Oct 21 '22 06:10

mdm