Last active
January 9, 2023 23:57
-
-
Save erdeszt/59bf8eb0e0e7eccc3e25f568c46e580c to your computer and use it in GitHub Desktop.
ZIO flavored CE error handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package zats | |
import cats.effect.{IO, IOApp} | |
import cats.effect.std.Console | |
import cats.{Applicative, Functor, MonadError} | |
import cats.syntax.functor.* | |
import cats.syntax.flatMap.* | |
import scala.annotation.implicitNotFound | |
type App[F[_]] = MonadError[F, Throwable] | |
object App: | |
def apply[F[_]](using app: App[F]): App[F] = app | |
def dieOnError[F[_]: App, E <: Throwable]: DieOnErrorPartiallyApplied[F, E] = | |
DieOnErrorPartiallyApplied[F, E]() | |
def catchSomeOrDie[F[_]: App, E <: Throwable]: CatchSomeOrDiePartiallyApplied[F, E] = | |
CatchSomeOrDiePartiallyApplied[F, E]() | |
// TODO: Fix apply auto application or rename(or remove PA and add return type to the original argument list) | |
class DieOnErrorPartiallyApplied[F[_]: App, E <: Throwable](): | |
def apply[A](action: Raise[F, E] ?=> F[A]): F[A] = | |
given raise: Raise[F, E] = Raise.appInstance[F, E] | |
App[F].handleErrorWith(action)(error => App[F].raiseError(error)) | |
class CatchSomeOrDiePartiallyApplied[F[_]: App, E <: Throwable](): | |
def apply[A](action: Raise[F, E] ?=> F[A])(handler: PartialFunction[E, F[A]]): F[A] = | |
given raise: Raise[F, E] = Raise.appInstance[F, E] | |
App[F].handleErrorWith(action) { error => | |
val refinedError = error.asInstanceOf[E] | |
if (handler.isDefinedAt(refinedError)) { | |
handler(refinedError) | |
} else { | |
App[F].raiseError(error) | |
} | |
} | |
@implicitNotFound("""Raise instance not found for error: ${E} and effect: ${F}. | |
You are using some code that can raise exceptions. | |
You need to either handle it with Handle or propagate it with Raise""") | |
trait Raise[F[_], -E]: | |
def raise(error: E): F[Nothing] | |
object Raise: | |
implicit def raiseIO[E <: Throwable]: Raise[IO, E] = new Raise: | |
def raise(error: E): IO[Nothing] = | |
IO.raiseError(error) | |
// NOTE: Should not be exported(maybe move to App) | |
def appInstance[F[_]: App, E <: Throwable]: Raise[F, E] = new Raise: | |
def raise(error: E): F[Nothing] = | |
App[F].raiseError(error) | |
@implicitNotFound("""Handle instance not found for error: ${E} and effect: ${F} | |
The default instance is for Handle[cats.effect.IO, E <: Throwable] | |
If your error type doesn't match """) | |
trait Handle[F[_], E]: | |
def handle[A](action: Raise[F, E] ?=> F[A])(handler: E => F[A]): F[A] | |
object Handle: | |
implicit def handleIO[E <: Throwable]: Handle[IO, E] = new Handle: | |
def handle[A](action: Raise[IO, E] ?=> IO[A])(handler: E => IO[A]): IO[A] = | |
action.handleErrorWith(e => handler(e.asInstanceOf[E])) | |
enum Error extends Throwable: | |
case Error1() | |
case Error2() | |
enum OtherError extends Throwable: | |
case DontCare() | |
case Care() | |
def fails[F[_]: App](using errors: Raise[F, Error]): F[Int] = | |
for | |
_ <- errors.raise(Error.Error1()) | |
_ <- App[F].raiseError(new RuntimeException("DIE")) | |
_ <- errors.raise(Error.Error2()) | |
// Type error, we can't throw too general errors | |
// _ <- errors.raise(new RuntimeException("oops")) | |
yield 5 | |
def dies[F[_]: App](using errors: Raise[F, OtherError]): F[Unit] = | |
for _ <- errors.raise(OtherError.DontCare()) | |
yield () | |
def resurrects[F[_]: App](using errors: Raise[F, OtherError]): F[Int] = | |
for _ <- errors.raise(OtherError.Care()) | |
yield 4 | |
def example[F[_]: App](using errors: Handle[F, Error]): F[Int] = | |
// Type error: can't handle errors that are too general | |
// errors.handle((e: Raise[F, Throwable]) => e.raise(new RuntimeException("oops")))(_ => F.pure(-1)) | |
for | |
_ <- App.catchSomeOrDie[F, OtherError].apply(resurrects) { case OtherError.Care() => | |
App[F].pure(-3) | |
} | |
_ <- App.dieOnError[F, OtherError].apply(dies[F]) | |
result <- errors.handle(fails)(_ => App[F].pure(-1)) | |
yield result | |
object Main extends IOApp.Simple: | |
override def run: IO[Unit] = | |
for | |
result <- App[IO].handleErrorWith(example) { fatal => | |
Console[IO].println(s"Fatal error happened: ${fatal}").as(-2) | |
} | |
_ <- Console[IO].println(s"Result: ${result}") | |
yield () |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment