diff --git a/core/src/main/scala/cats/mtl/Handle.scala b/core/src/main/scala/cats/mtl/Handle.scala index c935a384..85d9ab15 100644 --- a/core/src/main/scala/cats/mtl/Handle.scala +++ b/core/src/main/scala/cats/mtl/Handle.scala @@ -20,6 +20,7 @@ package mtl import cats.data._ import scala.annotation.implicitNotFound +import scala.util.control.NoStackTrace @implicitNotFound( "Could not find an implicit instance of Handle[${F}, ${E}]. If you\nhave a good way of handling errors of type ${E} at this location, you may want\nto construct a value of type EitherT for this call-site, rather than ${F}.\nAn example type:\n\n EitherT[${F}, ${E}, *]\n\nThis is analogous to writing try/catch around this call. The EitherT will\n\"catch\" the errors of type ${E}.\n\nIf you do not wish to handle errors of type ${E} at this location, you should\nadd an implicit parameter of this type to your function. For example:\n\n (implicit fhandle: Handle[${F}, ${E}}])\n") @@ -218,5 +219,40 @@ private[mtl] trait HandleInstances extends HandleLowPriorityInstances { } object Handle extends HandleInstances { + def apply[F[_], E](implicit ev: Handle[F, E]): Handle[F, E] = ev + + def ensure[F[_], E]: AdHocSyntax[F, E] = + new AdHocSyntax[F, E] + + final class AdHocSyntax[F[_], E] { + + def apply[A](body: Handle[F, E] => F[A])(implicit F: ApplicativeThrow[F]): Inner[A] = + new Inner(body) + + final class Inner[A](body: Handle[F, E] => F[A])(implicit F: ApplicativeThrow[F]) { + def recover(h: E => F[A]): F[A] = { + val Marker = new AnyRef + + def inner[B](fb: F[B])(f: E => F[B]): F[B] = + ApplicativeThrow[F].handleErrorWith(fb) { + case Submarine(e, Marker) => f(e.asInstanceOf[E]) + case t => ApplicativeThrow[F].raiseError(t) + } + + val fa = body(new Handle[F, E] { + def applicative = Applicative[F] + def raise[E2 <: E, B](e: E2): F[B] = + ApplicativeThrow[F].raiseError(Submarine(e, Marker)) + def handleWith[B](fb: F[B])(f: E => F[B]): F[B] = inner(fb)(f) + }) + + inner(fa)(h) + } + } + } + + private final case class Submarine[E](e: E, marker: AnyRef) + extends RuntimeException + with NoStackTrace } diff --git a/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala b/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala index 2967f8f0..5e601dae 100644 --- a/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala +++ b/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala @@ -18,9 +18,16 @@ package cats package mtl package tests -import cats.data.{Kleisli, WriterT} +import cats.data.{EitherT, Kleisli, WriterT} +import cats.laws.discipline.arbitrary._ +import cats.mtl.syntax.all._ +import cats.syntax.all._ + +import org.scalacheck.{Arbitrary, Cogen}, Arbitrary.arbitrary class HandleTests extends BaseSuite { + type F[A] = EitherT[Eval, Throwable, A] + test("handleForApplicativeError") { case class Foo[A](bar: A) @@ -39,4 +46,80 @@ class HandleTests extends BaseSuite { Handle[Kleisli[Foo, Unit, *], String] Handle[WriterT[Foo, String, *], String] } + + test("submerge custom errors") { + sealed trait Error extends Product with Serializable + + object Error { + case object First extends Error + case object Second extends Error + case object Third extends Error + } + + val test = + Handle.ensure[F, Error](implicit h => Error.Second.raise.as("nope")) recover { + case Error.First => "0".pure[F] + case Error.Second => "1".pure[F] + case Error.Third => "2".pure[F] + } + + assertEquals(test.value.value.toOption, Some("1")) + } + + test("submerge two independent errors") { + sealed trait Error1 extends Product with Serializable + + object Error1 { + case object First extends Error1 + case object Second extends Error1 + case object Third extends Error1 + } + + sealed trait Error2 extends Product with Serializable + + val test = Handle.ensure[F, Error1] { implicit h1 => + Handle.ensure[F, Error2] { implicit h2 => + // it's helpful to test the raise syntax infers even when multiple handles are present + val _ = h2 + Error1.Third.raise.as("nope") + } recover { e => e.toString.pure[F] } + } recover { + case Error1.First => "first1".pure[F] + case Error1.Second => "second1".pure[F] + case Error1.Third => "third1".pure[F] + } + + assertEquals(test.value.value.toOption, Some("third1")) + } + + { + final case class Error(value: Int) + + object Error { + implicit val arbError: Arbitrary[Error] = + Arbitrary(arbitrary[Int].flatMap(Error(_))) + + implicit val cogenError: Cogen[Error] = + Cogen((_: Error).value.toLong) + + implicit val eqError: Eq[Error] = + Eq.by((_: Error).value) + } + + implicit val eqThrowable: Eq[Throwable] = + Eq.fromUniversalEquals[Throwable] + + val test = Handle.ensure[F, Error] { implicit h => + EitherT liftF { + Eval later { + checkAll( + "Handle.ensure[F, Error]", + cats.mtl.laws.discipline.HandleTests[F, Error].handle[Int]) + } + } + } recover { case Error(_) => ().pure[F] } + + test.value.value + () + } }