Skip to content

Instantly share code, notes, and snippets.

@erdeszt
Last active January 9, 2023 23:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erdeszt/59bf8eb0e0e7eccc3e25f568c46e580c to your computer and use it in GitHub Desktop.
Save erdeszt/59bf8eb0e0e7eccc3e25f568c46e580c to your computer and use it in GitHub Desktop.
ZIO flavored CE error handling
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