-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add bridge between ApplicativeError and ApplicativeThrow #4616
base: main
Are you sure you want to change the base?
Changes from all commits
8408933
366d776
e29d1d6
945dfd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,12 @@ | |
|
||
package cats | ||
|
||
import cats.ApplicativeError.CatchOnlyPartiallyApplied | ||
import cats.ApplicativeError.{ | ||
CatchOnlyAsPartiallyApplied, | ||
CatchOnlyAsUnsafePartiallyApplied, | ||
CatchOnlyPartiallyApplied, | ||
CatchOnlySafePartiallyApplied | ||
} | ||
import cats.data.{EitherT, Validated} | ||
import cats.data.Validated.{Invalid, Valid} | ||
|
||
|
@@ -264,6 +269,8 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { | |
/** | ||
* Often E is Throwable. Here we try to call pure or catch | ||
* and raise. | ||
* | ||
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
*/ | ||
def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< E): F[A] = | ||
try pure(a) | ||
|
@@ -274,6 +281,8 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { | |
/** | ||
* Often E is Throwable. Here we try to call pure or catch | ||
* and raise | ||
* | ||
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
*/ | ||
def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< E): F[A] = | ||
try pure(a.value) | ||
|
@@ -282,11 +291,85 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { | |
} | ||
|
||
/** | ||
* Evaluates the specified block, catching exceptions of the specified type. Uncaught exceptions are propagated. | ||
* Evaluates the specified block, catching exceptions of the specified type. | ||
* | ||
* This method is considered unsafe because uncaught exceptions are propagated | ||
* outside of `F`. While the name does not currently reflect this, this may change | ||
* at a future date. | ||
*/ | ||
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T, F, E] = | ||
new CatchOnlyPartiallyApplied[T, F, E](this) | ||
|
||
/** | ||
* Evaluates the specified block, catching exceptions of the specified type. | ||
* | ||
* Exceptions of type `T` are raised in `F`, other non-fatal exceptions are raised in `G` | ||
* | ||
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
*/ | ||
def catchOnlySafe[G[_], T >: Null <: Throwable]: CatchOnlySafePartiallyApplied[T, G, F, E] = | ||
new CatchOnlySafePartiallyApplied[T, G, F, E](this) | ||
|
||
/** | ||
* Often E can be created from Throwable. Here we try to call pure or | ||
* catch, adapt into E, and raise. | ||
* | ||
* Exceptions that cannot be adapted to E and raised in `F` will be raised in `G` | ||
* | ||
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
*/ | ||
def catchNonFatalAs[G[_], A](adaptIfPossible: Throwable => Option[E])(a: => A)(implicit | ||
G: ApplicativeThrow[G] | ||
): G[F[A]] = | ||
try G.pure(pure(a)) | ||
catch { | ||
case t if NonFatal(t) => | ||
adaptIfPossible(t).fold(G.raiseError[F[A]](t))(e => G.pure(raiseError[A](e))) | ||
} | ||
|
||
/** | ||
* Often E can be created from Throwable. Here we try to call pure or | ||
* catch, adapt into E, and raise. | ||
* | ||
* This method is considered unsafe because exceptions that cannot be | ||
* adapted to E will be rethrown outside of `F` | ||
* | ||
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
*/ | ||
def catchNonFatalAsUnsafe[A](adaptIfPossible: Throwable => Option[E])(a: => A): F[A] = | ||
try pure(a) | ||
catch { | ||
case e if NonFatal(e) => adaptIfPossible(e).map(raiseError[A]).getOrElse(throw e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line concerns me quite a lot, particularly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless we require the adaptation function to be total, we're going to need to rethrow these, because there isn't a way to raise a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exactly. This is why we must not use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @armanbilge , |
||
} | ||
|
||
/** | ||
* Evaluates the specified block, catching exceptions of the specified type. | ||
* | ||
* Caught exceptions are mapped to `E` and raised in `F`. | ||
* Uncaught non-fatal exceptions will be raised in `G`. | ||
* Fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated | ||
* | ||
* Note: `catchOnlyAs` assumes that, if a specific exception type `T` is expected, there | ||
* exists a mapping from `T` to `E`. If this is not the case, consider either manually | ||
* rethrowing inside the mapping function, or using `catchNonFatalAs` | ||
*/ | ||
def catchOnlyAs[G[_], T >: Null <: Throwable]: CatchOnlyAsPartiallyApplied[T, G, F, E] = | ||
new CatchOnlyAsPartiallyApplied[T, G, F, E](this) | ||
|
||
/** | ||
* Evaluates the specified block, catching exceptions of the specified type. | ||
* | ||
* Caught exceptions are mapped to `E` and raised in `F`. | ||
* | ||
* This method is considered unsafe because uncaught exceptions will be propagated outside of `F` | ||
* | ||
* Note: `catchOnlyAsUnsafe` assumes that, if a specific exception type `T` is expected, there | ||
* exists a mapping from `T` to `E`. If this is not the case, consider either manually | ||
* rethrowing inside the mapping function, or using `catchNonFatalAsUnsafe` | ||
*/ | ||
def catchOnlyAsUnsafe[T >: Null <: Throwable]: CatchOnlyAsUnsafePartiallyApplied[T, F, E] = | ||
new CatchOnlyAsUnsafePartiallyApplied[T, F, E](this) | ||
|
||
/** | ||
* If the error type is Throwable, we can convert from a scala.util.Try | ||
*/ | ||
|
@@ -383,6 +466,39 @@ object ApplicativeError { | |
} | ||
} | ||
|
||
final private[cats] class CatchOnlySafePartiallyApplied[T, G[_], F[_], E](private val F: ApplicativeError[F, E]) | ||
extends AnyVal { | ||
def apply[A]( | ||
f: => A | ||
)(implicit CT: ClassTag[T], NT: NotNull[T], ev: Throwable <:< E, G: ApplicativeThrow[G]): G[F[A]] = | ||
try G.pure(F.pure(f)) | ||
catch { | ||
case t if CT.runtimeClass.isInstance(t) => G.pure(F.raiseError(t)) | ||
case t if NonFatal(t) => G.raiseError(t) | ||
} | ||
} | ||
|
||
final private[cats] class CatchOnlyAsPartiallyApplied[T >: Null <: Throwable, G[_], F[_], E]( | ||
private val F: ApplicativeError[F, E] | ||
) extends AnyVal { | ||
def apply[A](adapt: T => E)(f: => A)(implicit CT: ClassTag[T], NT: NotNull[T], G: ApplicativeThrow[G]): G[F[A]] = | ||
try G.pure(F.pure(f)) | ||
catch { | ||
case CT(t) => G.pure(F.raiseError(adapt(t))) | ||
case t if NonFatal(t) => G.raiseError[F[A]](t) | ||
} | ||
} | ||
|
||
final private[cats] class CatchOnlyAsUnsafePartiallyApplied[T >: Null <: Throwable, F[_], E]( | ||
private val F: ApplicativeError[F, E] | ||
) extends AnyVal { | ||
def apply[A](adapt: T => E)(f: => A)(implicit CT: ClassTag[T], NT: NotNull[T]): F[A] = | ||
try F.pure(f) | ||
catch { | ||
case CT(t) => F.raiseError(adapt(t)) | ||
} | ||
} | ||
|
||
/** | ||
* lift from scala.Option[A] to a F[A] | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I find such a naming quite confusing. I feel in Cats everything is supposed to be safe, unless stated and marked otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No argument, I just needed to name it something so I could put a safe variant of
catchOnly
in there so we can figure out if it's something worth adding or not, as renaming the existing method tocatchOnlyUnsafe
would break bincompat