-
-
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 2 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,7 @@ | |
|
||
package cats | ||
|
||
import cats.ApplicativeError.CatchOnlyPartiallyApplied | ||
import cats.ApplicativeError.{CatchOnlyAsPartiallyApplied, CatchOnlyPartiallyApplied} | ||
import cats.data.{EitherT, Validated} | ||
import cats.data.Validated.{Invalid, Valid} | ||
|
||
|
@@ -287,6 +287,30 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { | |
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T, F, E] = | ||
new CatchOnlyPartiallyApplied[T, 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 will be propagated outside of `F` | ||
*/ | ||
def catchNonFatalAs[A](adaptIfPossible: Throwable => Option[E])(a: => A): F[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. I would probably prefer to deal with
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. Not mentioning that the handler would look more like a regular 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. Agreed, using PFs for error handling is a common practice 👍🏻 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. I would expect most times ApplicativeError[Either[E, *], E].catchNonFatalAs(E.fromThrowable) {
// block
} I'm open to being wrong about this, I just expect this sort of transformation won't generally written at the call sites because, if you need to do this, you probably need to do it more than once. |
||
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 and maps them to `E`. | ||
* | ||
* Uncaught exceptions will be propagated outside of `F` | ||
* | ||
* 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[T >: Null <: Throwable]: CatchOnlyAsPartiallyApplied[T, F, E] = | ||
morgen-peschke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
new CatchOnlyAsPartiallyApplied[T, F, E](this) | ||
|
||
/** | ||
* If the error type is Throwable, we can convert from a scala.util.Try | ||
*/ | ||
|
@@ -383,6 +407,16 @@ object ApplicativeError { | |
} | ||
} | ||
|
||
final private[cats] class CatchOnlyAsPartiallyApplied[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.
Repeating my comments from #4286 (comment):
This doesn't seem right to me. Exception-throwing code should ideally never be written outside of a
catchNonFatal
; pure code that uses this method (or any method in Cats) should not throw exceptions.It seems to me what you actually want is maybe something like
F[G[A]]
, withApplicativeError[G, E]
andApplicativeThrow[F]
. That way non-adaptable exceptions can be safely handled in theF
effect.In which case I think you want something like this?
Maybe a dedicated method/syntax could make that a bit neater. I'm not entirely sure where it could live.
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.
Adding the extra effect layer would make these incongruent with
catchOnly
, which also throws exceptions that aren't expected, rather than attempting to raise them inF
.If there were a less clunky way to do the
.map(G.pure).recover { ... }
, that would improve the ergonomics enough for the usecase to basically disappear intoSync[F].delay
+ whatever the improvement is.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.
Honestly, I personally have never used
catchOnly
before, and never paid attention to it. And now I'm a bit surprised that such a function even exists in here.On the other hand though, there are several examples in Cats when a function can throw an exception, e.g.
NonEmptyList.fromListUnsafe
and alike. It is a slightly different case though, because happens not in a type class but rather in a particular data type. But still close.From that point of view, if we want to have functions in Cats like
catchOnly
or similar that can (re)throw exceptions, then perhaps it makes sense to mark them "unsafe", because they really are, e.g.:unsafeCatchOnly
orcatchOnlyUnsafe
. And also explain in the docs, why they are unsafe.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.
Would the path forward then be to rename/deprecate the existing
catch*
methods?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.
I think this PR would be better off staying focused on its initial goal.
Perhaps, it makes sense to provide a safe version of
catchNonFatalAs
as @armanbilge suggested along with its unsafe counterpart.The effort on renaming of the existing unsafe methods could be addressed in a separate PR later on. Actually, not all the existing
catch*
methods look unsafe to me, onlycatchOnly
does. On the other hand, not onlyApplicativeError
containscatchOnly
but alsoValidated
andEitherSyntax
have their specialized versions that can be considered unsafe in the same way. So the renaming is likely worth a separate PR.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.
I figured the easiest way to advance would be to implement
*Unsafe
versions of the proposed methods, and acatchOnlySafe
version ofcatchOnly
.This way we have concrete implementations to look at and figure out which add value.