Error management
Producing errors
Section titled “Producing errors”Problem
Section titled “Problem”One of the major issues of the MTL style is an error handling.
The weakest Cats
typeclass, which enables operations with errors, is an
ApplicativeError
.
It brings a full Applicative
instance apart from error-related methods and
this means, that we are not allowed to have a few FunctorRaise
or ApplicativeError
instances in the scope, since
their underlying Functor
/Applicative
instances will come into conflict:
import cats._ case class ArithmeticError() extends Throwable case class ParseError() extends Throwable
def divideBad[F[_]](x: String, y: String)(implicit F1: ApplicativeError[F, ArithmeticError], F2: ApplicativeError[F, ParseError]): F[String] = // using Functor / Applicative syntax here will cause an // "ambiguous implicit values" error ???
So we are forced to choose a single unified error type.
Solution
Section titled “Solution”The simplest solution here is to create a typeclass, that is not a subtype of Functor:
trait Raise[F[_], E]{ def raise[A](err: E): F[A]}
(see also
cats-mtl ‘s FunctorRaise
).
It would allow us to distinguish between different types of errors:
import cats.effect.IOimport tofu._import tofu.syntax.monadic._import tofu.syntax.raise._
def divide[F[_]: Monad](x: String, y: String)(implicit F1: Raise[F, ArithmeticError], F2: Raise[F, ParseError] ): F[String] = ( x.toIntOption.orRaise(ParseError()), y.toIntOption.orRaise(ParseError()) .verified(_ != 0)(ArithmeticError()) ).mapN(_ / _).map(_.toString)
divide[IO]("10", "3").attempt.unsafeRunSync()
divide[IO]("10","0").attempt.unsafeRunSync()
divide[IO]("1", "0").attempt.unsafeRunSync()
Recovering from errors
Section titled “Recovering from errors”Problem
Section titled “Problem”ApplicativeError
provides the following method for error handling:
def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
Here, if f
does not fail, F[A]
should describe a successful computation. The types, however, do not convey this fact,
since we have no type for Unexeptional
partner. Read more here
Solution
Section titled “Solution”Tofu
is shipped with a few typeclasses targeting the problem. The simplest one is
trait RestoreTo[F[_], G[_]] { def restore[A](fa: F[A]): G[Option[A]]}
which can be used to restore from any failure condition.
Another one is
trait HandleTo[F[_], G[_], E] extends RestoreTo[F, G] { def handleWith[A](fa: F[A])(f: E => G[A]): G[A]
def handle[A](fa: F[A])(f: E => A)(implicit G: Applicative[G]): G[A] = handleWith(fa)(e => G.pure(f(e)))
def attempt[A](fa: F[A])(implicit F: Functor[F], G: Applicative[G]): G[Either[E, A]] = handle(F.map(fa)(_.asRight[E]))(_.asLeft)}
which can handle concrete error type:
import cats._import cats.data.EitherTimport cats.instances.vector._import cats.syntax.foldable._import cats.syntax.traverse._import tofu._import tofu.syntax.handle._import tofu.syntax.monadic._import tofu.syntax.raise._
def splitErrors[ T[_]: Traverse: Alternative, F[_]: Functor, G[_]: Applicative, E, A](ls: T[F[A]])( implicit errors: ErrorsTo[F, G, E]): G[(T[E], T[A])] = ls.traverse(_.attemptTo[G, E]).map(_.partitionEither(identity))
def parseInt[F[_]: Applicative: Raise[*[_], String]](s: String): F[Int] = s.toIntOption.orRaise(s"could not parse $s")
type Calc[A] = EitherT[Eval, String, A]
splitErrors[Vector, Calc, Eval, String, Int]( Vector("1", "hello", "2", "world", "3").map(parseInt[Calc])).value
HandleTo, empowered with Raise, is called ErrorsTo
:
trait ErrorsTo[F[_], G[_], E] extends Raise[F, E] with HandleTo[F, G, E]
There are also specialized versions of RestoreTo
, HandleTo
and ErrorsTo
without To
:
trait Restore[F[_]] extends RestoreTo[F, F] { def restoreWith[A](fa: F[A])(ra: => F[A]): F[A]}
trait Handle[F[_], E] extends HandleTo[F, F, E] with Restore[F] {
def tryHandleWith[A](fa: F[A])(f: E => Option[F[A]]): F[A]
def tryHandle[A](fa: F[A])(f: E => Option[A])(implicit F: Applicative[F]): F[A] = tryHandleWith(fa)(e => f(e).map(F.pure))
def handleWith[A](fa: F[A])(f: E => F[A]): F[A] = tryHandleWith(fa)(e => Some(f(e)))
def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] = tryHandleWith(fa)(pf.lift)
def recover[A](fa: F[A])(pf: PartialFunction[E, A])(implicit F: Applicative[F]): F[A] = tryHandle(fa)(pf.lift)
def restoreWith[A](fa: F[A])(ra: => F[A]): F[A] = handleWith(fa)(_ => ra)}
trait Errors[F[_], E] extends Raise[F, E] with Handle[F, E] with ErrorsTo[F, F, E]