Home > Software engineering >  Scala with cats IO/Either instead of Future/Exceptions
Scala with cats IO/Either instead of Future/Exceptions

Time:11-04

I'm in the process of adopting IO/Either to replace Future/Exception's where applicable, but I need help with the following code:

// some Java library
def dbLoad(id: Int): Int = {
  throw new Exception("db exception")
}

// my scala code
sealed trait DbError extends Exception with Product

object DbError {
  case object SomeError extends DbError
}

val load: Int => IO[Either[DbError, Int]] = { id =>
  IO.fromFuture { IO { Future {
    try { Right(dbLoad(id)) } catch { case NonFatal(e) => Left(SomeError) }
  } } }
}

val loadAll: IO[Either[DbError, (Int, Int, Int)]] =
  for {
    i1 <- load(1)
    i2 <- // call 'load' passing i1 as parameter, if i1 is 'right'
    i3 <- // call 'load' passing i2 as parameter, if i2 is 'right'
  } yield (i1, i2, i3) match {
    case (Right(i1), Right(i2), Right(i3)) => Right((i1, i2, i3))
    case _ => Left(SomeError)
  }

I have not been able to get it working/compiling correctly, could you please help me understand:

  1. how can I avoid executing subsequent calls to load (in loadAll) if a Left is detected?
  2. if a call to load is successful, how can I use its right value for the following call to load?
  3. is this the right approach to do it? Would you implement it in a different way?

Thanks everyone

CodePudding user response:

Let me first put up the code that I think gets you what you want, and how typically things like this might be approached, I'll then describe what and why, and maybe some other suggestions:

import cats.data.EitherT
import cats.effect.IO
import cats.implicits._
import com.example.StackOverflow.DbError.SomeError

import scala.concurrent.Future
import scala.util.control.NonFatal
import scala.concurrent.ExecutionContext.Implicits.global

object StackOverflow {
  // some Java library
  def dbLoad(id: Int): Int = {
    throw new Exception("db exception")
  }

  // my scala code
  sealed trait DbError extends Exception with Product

  object DbError {
    case object SomeError extends DbError
  }

  val load: Int => IO[Either[DbError, Int]] = { id =>
    IO.fromFuture(
      IO(
        Future(dbLoad(id))
          .map(Right(_))
          .recover {
            case NonFatal(_) => Left(SomeError)
          }
      )
    )
  }

  val loadAll: EitherT[IO, DbError, (Int, Int, Int)] =
    for {
      i1 <- EitherT(load(1))
      i2 <- EitherT(load(i1))
      i3 <- EitherT(load(i2))
    } yield (i1, i2, i3)

  val x: IO[Either[DbError, (Int, Int, Int)]] = loadAll.value
}

First, rather than try-catch inside the IO[Future[_]], Future itself has a number of combinators that can help you manage errors, assuming you have some control over what you get back.

For-comprehensions in Scala when applied in this fashion "short circuit" so if the first call to load(1) fails with a left then the rest of the comprehension won't execute. The use of EitherT allows you to manage the fact that your Either is "wrapped" in an effect type.

There are some problems with this approach, specifically around variance, you can read about them here:

http://www.beyondthelines.net/programming/the-problem-with-eithert/

There are also some performance implications for using this pattern that you may want to consider

  • Related