Skip to content

Commit db33147

Browse files
authored
Merge pull request #664 from iRevive/topic/handle-attempt
Add Handle#allow#attempt
2 parents df06166 + 456d47b commit db33147

4 files changed

Lines changed: 102 additions & 12 deletions

File tree

core/src/main/scala-3/cats/mtl/HandleCrossCompat.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
package cats
1818
package mtl
1919

20+
import cats.syntax.applicative.*
21+
import cats.syntax.either.*
22+
import cats.syntax.functor.*
23+
2024
private[mtl] trait HandleCrossCompat:
2125

2226
inline def allow[E]: AdHocSyntaxWired[E] =
@@ -35,6 +39,13 @@ private final class InnerWired[F[_], E, A](body: Handle[F, E] => F[A]) extends A
3539

3640
inner(body(InnerHandle(Marker)), f, Marker)
3741

42+
inline def attempt(using ApplicativeThrow[F]): F[Either[E, A]] =
43+
val Marker = new AnyRef
44+
inner[F, E, Either[E, A]](
45+
body(InnerHandle(Marker)).map(_.asRight),
46+
_.asLeft.pure[F],
47+
Marker)
48+
3849
private inline def inner[F[_], E, A](inline fb: F[A], inline f: E => F[A], Marker: AnyRef)(
3950
using ApplicativeThrow[F]): F[A] =
4051
ApplicativeThrow[F].handleErrorWith(fb):

core/src/main/scala/cats/mtl/Handle.scala

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package cats
1818
package mtl
1919

2020
import cats.data._
21+
import cats.syntax.applicative._
22+
import cats.syntax.either._
23+
import cats.syntax.functor._
2124

2225
import scala.annotation.implicitNotFound
2326
import scala.util.control.NoStackTrace
@@ -235,21 +238,37 @@ object Handle extends HandleInstances with HandleCrossCompat {
235238
def rescue(h: E => F[A])(implicit F: ApplicativeThrow[F]): F[A] = {
236239
val Marker = new AnyRef
237240

238-
def inner[B](fb: F[B])(f: E => F[B]): F[B] =
239-
ApplicativeThrow[F].handleErrorWith(fb) {
240-
case Submarine(e, Marker) => f(e.asInstanceOf[E])
241-
case t => ApplicativeThrow[F].raiseError(t)
242-
}
241+
val fa = body(new InnerHandle(Marker))
242+
243+
inner(fa, h, Marker)
244+
}
245+
246+
def attempt(implicit F: ApplicativeThrow[F]): F[Either[E, A]] = {
247+
val Marker = new AnyRef
243248

244-
val fa = body(new Handle[F, E] {
245-
def applicative = Applicative[F]
246-
def raise[E2 <: E, B](e: E2): F[B] =
247-
ApplicativeThrow[F].raiseError(Submarine(e, Marker))
248-
def handleWith[B](fb: F[B])(f: E => F[B]): F[B] = inner(fb)(f)
249-
})
249+
val fa = body(new InnerHandle(Marker))
250250

251-
inner(fa)(h)
251+
inner(fa.map(_.asRight), _.asLeft.pure[F], Marker)
252252
}
253+
254+
private def inner[B](fb: F[B], f: E => F[B], Marker: AnyRef)(
255+
implicit F: ApplicativeThrow[F]): F[B] =
256+
ApplicativeThrow[F].handleErrorWith(fb) {
257+
case Submarine(e, Marker) => f(e.asInstanceOf[E])
258+
case t => ApplicativeThrow[F].raiseError(t)
259+
}
260+
261+
}
262+
263+
private final class InnerHandle[F[_]: ApplicativeThrow, E](Marker: AnyRef)
264+
extends Handle[F, E] {
265+
def applicative = Applicative[F]
266+
def raise[E2 <: E, B](e: E2): F[B] = ApplicativeThrow[F].raiseError(Submarine(e, Marker))
267+
def handleWith[B](fb: F[B])(f: E => F[B]): F[B] =
268+
ApplicativeThrow[F].handleErrorWith(fb) {
269+
case Submarine(e, Marker) => f(e.asInstanceOf[E])
270+
case t => ApplicativeThrow[F].raiseError(t)
271+
}
253272
}
254273

255274
private[mtl] final case class Submarine[E](e: E, marker: AnyRef)

tests/shared/src/test/scala-3/cats/mtl/tests/Handle3Tests.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,33 @@ class Handle3Tests extends munit.FunSuite:
7272
case Error1.Third => "third1".pure[F]
7373
case Error2.Fourth => "fourth1".pure[F]
7474
assert.equals(test.value.value.toOption, Some("third1"))
75+
76+
test("attempt - return Either[E, A]"):
77+
enum Error:
78+
case First, Second, Third
79+
80+
val success: F[Either[Error, String]] =
81+
allow[Error]:
82+
EitherT.rightT[Eval, Throwable]("all good")
83+
.attempt
84+
85+
val failure =
86+
allow[Error]:
87+
Error.Second.raise[F, String].as("nope")
88+
.attempt
89+
90+
assert(success.value.value == Right(Right("all good")))
91+
assert(failure.value.value == Right(Left(Error.Second)))
92+
93+
test("attempt - propagate unhandled exceptions"):
94+
enum Error:
95+
case First, Second, Third
96+
97+
val exception = new RuntimeException("oops")
98+
99+
val test =
100+
allow[Error]:
101+
EitherT.leftT[Eval, Unit](exception)
102+
.attempt
103+
104+
assert(test.value.value == Left(exception))

tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,36 @@ class HandleTests extends BaseSuite {
9292
assert(test.value.value.toOption == Some("third1"))
9393
}
9494

95+
test("attempt - return Either[E, A]") {
96+
sealed trait Error extends Product with Serializable
97+
98+
object Error {
99+
case object First extends Error
100+
case object Second extends Error
101+
case object Third extends Error
102+
}
103+
104+
val success =
105+
Handle.allowF[F, Error](_ => EitherT.pure("all good")).attempt
106+
107+
val failure =
108+
Handle.allowF[F, Error](implicit h => Error.Second.raise.as("nope")).attempt
109+
110+
assert(success.value.value == Right(Right("all good")))
111+
assert(failure.value.value == Right(Left(Error.Second)))
112+
}
113+
114+
test("attempt - propagate unhandled exceptions") {
115+
sealed trait Error extends Product with Serializable
116+
117+
val exception = new RuntimeException("oops")
118+
119+
val test =
120+
Handle.allowF[F, Error](_ => EitherT.leftT[Eval, Unit](exception)).attempt
121+
122+
assert(test.value.value == Left(exception))
123+
}
124+
95125
{
96126
final case class Error(value: Int)
97127

0 commit comments

Comments
 (0)