Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/src/main/scala-3/cats/mtl/HandleCrossCompat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
package cats
package mtl

import cats.syntax.applicative.*
import cats.syntax.either.*
import cats.syntax.functor.*

private[mtl] trait HandleCrossCompat:

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

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

inline def attempt(using ApplicativeThrow[F]): F[Either[E, A]] =
val Marker = new AnyRef
inner[F, E, Either[E, A]](
body(InnerHandle(Marker)).map(_.asRight),
_.asLeft.pure[F],
Marker)

private inline def inner[F[_], E, A](inline fb: F[A], inline f: E => F[A], Marker: AnyRef)(
using ApplicativeThrow[F]): F[A] =
ApplicativeThrow[F].handleErrorWith(fb):
Expand Down
43 changes: 31 additions & 12 deletions core/src/main/scala/cats/mtl/Handle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package cats
package mtl

import cats.data._
import cats.syntax.applicative._
import cats.syntax.either._
import cats.syntax.functor._

import scala.annotation.implicitNotFound
import scala.util.control.NoStackTrace
Expand Down Expand Up @@ -235,21 +238,37 @@ object Handle extends HandleInstances with HandleCrossCompat {
def rescue(h: E => F[A])(implicit F: ApplicativeThrow[F]): 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 InnerHandle(Marker))

inner(fa, h, Marker)
}

def attempt(implicit F: ApplicativeThrow[F]): F[Either[E, A]] = {
val Marker = new AnyRef

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)
})
val fa = body(new InnerHandle(Marker))

inner(fa)(h)
inner(fa.map(_.asRight), _.asLeft.pure[F], Marker)
}

private def inner[B](fb: F[B], f: E => F[B], Marker: AnyRef)(
implicit F: ApplicativeThrow[F]): F[B] =
ApplicativeThrow[F].handleErrorWith(fb) {
case Submarine(e, Marker) => f(e.asInstanceOf[E])
case t => ApplicativeThrow[F].raiseError(t)
}

}

private final class InnerHandle[F[_]: ApplicativeThrow, E](Marker: AnyRef)
extends 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] =
ApplicativeThrow[F].handleErrorWith(fb) {
case Submarine(e, Marker) => f(e.asInstanceOf[E])
case t => ApplicativeThrow[F].raiseError(t)
}
}

private[mtl] final case class Submarine[E](e: E, marker: AnyRef)
Expand Down
30 changes: 30 additions & 0 deletions tests/shared/src/test/scala-3/cats/mtl/tests/Handle3Tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,33 @@ class Handle3Tests extends munit.FunSuite:
case Error1.Third => "third1".pure[F]
case Error2.Fourth => "fourth1".pure[F]
assert.equals(test.value.value.toOption, Some("third1"))

test("attempt - return Either[E, A]"):
enum Error:
case First, Second, Third

val success: F[Either[Error, String]] =
allow[Error]:
EitherT.rightT[Eval, Throwable]("all good")
.attempt

val failure =
allow[Error]:
Error.Second.raise[F, String].as("nope")
.attempt

assert(success.value.value == Right(Right("all good")))
assert(failure.value.value == Right(Left(Error.Second)))

test("attempt - propagate unhandled exceptions"):
enum Error:
case First, Second, Third

val exception = new RuntimeException("oops")

val test =
allow[Error]:
EitherT.leftT[Eval, Unit](exception)
.attempt

assert(test.value.value == Left(exception))
30 changes: 30 additions & 0 deletions tests/shared/src/test/scala/cats/mtl/tests/HandleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,36 @@ class HandleTests extends BaseSuite {
assert(test.value.value.toOption == Some("third1"))
}

test("attempt - return Either[E, A]") {
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 success =
Handle.allowF[F, Error](_ => EitherT.pure("all good")).attempt

val failure =
Handle.allowF[F, Error](implicit h => Error.Second.raise.as("nope")).attempt

assert(success.value.value == Right(Right("all good")))
assert(failure.value.value == Right(Left(Error.Second)))
}

test("attempt - propagate unhandled exceptions") {
sealed trait Error extends Product with Serializable

val exception = new RuntimeException("oops")

val test =
Handle.allowF[F, Error](_ => EitherT.leftT[Eval, Unit](exception)).attempt

assert(test.value.value == Left(exception))
}

{
final case class Error(value: Int)

Expand Down