From c98c2aef2ecfbdca88bde01f0cd4ba02be6820a8 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 11:04:31 -0600 Subject: [PATCH 1/6] Implemented submarine error propagation for `Handle` --- core/src/main/scala/cats/mtl/Handle.scala | 36 ++++++++ .../cats/mtl/tests/AdHocHandleTests.scala | 90 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala 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/AdHocHandleTests.scala b/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala new file mode 100644 index 00000000..bc0968c6 --- /dev/null +++ b/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala @@ -0,0 +1,90 @@ +package cats +package mtl +package tests + +import cats.data.EitherT +import cats.laws.discipline.arbitrary._ +import cats.mtl.laws.discipline.HandleTests +import cats.mtl.syntax.all._ +import cats.syntax.all._ + +import org.scalacheck.{Arbitrary, Cogen}, Arbitrary.arbitrary + +class AdHocHandleTests extends BaseSuite { + + type F[A] = EitherT[Eval, Throwable, A] + + 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] + } + + assert(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 => + val _ = + h2 // it's helpful to test the raise syntax infers even when multiple handles are present + 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] + } + + assert(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]", HandleTests[F, Error].handle[Int]) + } + } + } recover { case Error(_) => ().pure[F] } + + test.value.value + () + } +} From 1a4e0e8b8856ea3aa5a110600491e080c23407c3 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 11:17:31 -0600 Subject: [PATCH 2/6] Added headers --- .../scala/cats/mtl/tests/AdHocHandleTests.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala b/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala index bc0968c6..f14b3595 100644 --- a/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala +++ b/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cats package mtl package tests From f044fabe0ff67d2fbaaa28e9d3117d40188e5f2c Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 11:53:46 -0600 Subject: [PATCH 3/6] Collapsed `Handle` tests and fixed Scala 3 issues --- .../cats/mtl/tests/AdHocHandleTests.scala | 106 ------------------ .../scala/cats/mtl/tests/HandleTests.scala | 85 +++++++++++++- 2 files changed, 84 insertions(+), 107 deletions(-) delete mode 100644 tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala diff --git a/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala b/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala deleted file mode 100644 index f14b3595..00000000 --- a/tests/shared/src/test/scala/cats/mtl/tests/AdHocHandleTests.scala +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats -package mtl -package tests - -import cats.data.EitherT -import cats.laws.discipline.arbitrary._ -import cats.mtl.laws.discipline.HandleTests -import cats.mtl.syntax.all._ -import cats.syntax.all._ - -import org.scalacheck.{Arbitrary, Cogen}, Arbitrary.arbitrary - -class AdHocHandleTests extends BaseSuite { - - type F[A] = EitherT[Eval, Throwable, A] - - 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] - } - - assert(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 => - val _ = - h2 // it's helpful to test the raise syntax infers even when multiple handles are present - 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] - } - - assert(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]", HandleTests[F, Error].handle[Int]) - } - } - } recover { case Error(_) => ().pure[F] } - - test.value.value - () - } -} 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..4ab00f3d 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] + } + + assert(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 => + val _ = + h2 // it's helpful to test the raise syntax infers even when multiple handles are present + 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] + } + + assert(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 + () + } } From 5dee0bfcf69a6ae3df90f6cfb9b9177426268aeb Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 15:49:59 -0600 Subject: [PATCH 4/6] Update tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala Co-authored-by: Arman Bilge --- tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4ab00f3d..7600d242 100644 --- a/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala +++ b/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala @@ -79,8 +79,8 @@ class HandleTests extends BaseSuite { val test = Handle.ensure[F, Error1] { implicit h1 => Handle.ensure[F, Error2] { implicit h2 => - val _ = - h2 // it's helpful to test the raise syntax infers even when multiple handles are present + // 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 { From 07c8aeb1990a651fb01dde98e79712d0c772aeca Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 18:35:19 -0600 Subject: [PATCH 5/6] Update tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala Co-authored-by: Arman Bilge --- tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7600d242..5a1c5ad9 100644 --- a/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala +++ b/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala @@ -63,7 +63,7 @@ class HandleTests extends BaseSuite { case Error.Third => "2".pure[F] } - assert(test.value.value.toOption == Some("1")) + assertEquals(test.value.value.toOption, Some("1")) } test("submerge two independent errors") { From 0b8e8975d4c4722550c9bd9305df5a1ab0c0fd2b Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 30 Apr 2023 18:35:24 -0600 Subject: [PATCH 6/6] Update tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala Co-authored-by: Arman Bilge --- tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5a1c5ad9..5e601dae 100644 --- a/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala +++ b/tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala @@ -89,7 +89,7 @@ class HandleTests extends BaseSuite { case Error1.Third => "third1".pure[F] } - assert(test.value.value.toOption == Some("third1")) + assertEquals(test.value.value.toOption, Some("third1")) } {