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:
- how can I avoid executing subsequent calls to
load
(inloadAll
) if aLeft
is detected? - if a call to
load
is successful, how can I use itsright
value for the following call toload
? - 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