Make IO#onError consistent with ApplicativeError#4121
Conversation
This also deprecates the old `IO.onError` function.
f9ae95c to
b4cd5e5
Compare
As it requires to annotate anonymous function: The argument types of an anonymous function must be fully known. (SLS 8.5)
9aedbfe to
7825274
Compare
IO#onError consistent with ApplicativeError
|
|
||
| @deprecated("Use onError with PartialFunction argument", "3.6.0") | ||
| def onError(f: Throwable => IO[Unit]): IO[A] = | ||
| handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) |
There was a problem hiding this comment.
Can we delegate this to the other method?
| handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) | |
| onError(f: PartialFunction[Throwable, IO[Unit]]) |
There was a problem hiding this comment.
mm, I'm not sure I understand, I think your suggestion shouldn't be compiled?
Or do you mean use the new onError?
btw, I'm about to go to bed, will think more and answer tomorrow.
There was a problem hiding this comment.
I think your suggestion shouldn't be compiled? Or do you mean use the new
onError?
Oops, I made a typo. But I think you are right, this won't compile as-is 🤔
Basically we should avoid to duplicate the onError implementation. We can share the implementation by delegating one onError to the other onError, by converting Function to PartialFunction or vice versa.
There was a problem hiding this comment.
Fixed this in f4f505c
Another way would be
- handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t))
+ onError(t => pf.applyOrElse(t, (_: Throwable) => IO.unit))but it feels awkward as We used a function that We just deprecated it.
| * Implements `ApplicativeError.onError`. | ||
| */ | ||
| def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = | ||
| handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t)) |
There was a problem hiding this comment.
To match the other implementation we need this voidError. But this raises an interesting question about the semantics: if the onError callback raises an error, which error should be propagated, that error or the original one? Also the voidError completely discards the error without reporting it, which seems really weird 😕
| handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t)) | |
| handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit).voidError *> IO.raiseError(t)) |
There was a problem hiding this comment.
I think what we should do is extract this into a helper method.
Then instead of voidError we should redirect errors within the onError callback to that? And then re-raise the original error.
There was a problem hiding this comment.
thanks @armanbilge, I'm completely missed that voidError. I added reportError helper method as your suggestion. I think this is a great improvement. The name is subjecting for bike-shedding. We can also use this reportError method in the two other places. I can do that in this pr.
There was a problem hiding this comment.
I'm completely missed that
voidError
Yes, I almost missed it as well. We should add a test to cover this case (what happens if onError callback raises an error), that would have helped us. Actually we might even consider adding this as a law to Cats, so that onError semantics are the same across all implementations.
We can also use this
reportErrormethod in the two other places. I can do that in this pr.
🚀 it looks good! I think name is reasonable and it is private internal anyway.
There was a problem hiding this comment.
cool, I'll add a test case for in this first. And think about a law for cats later on.
There was a problem hiding this comment.
Hmm, actually, now I am hesitating 😅
The goal of this PR is to make IO#onError consistent with ApplicativeError#onError. If we use voidError/reportError then the semantics are inconsistent again with ApplicativeError 😕
Unless we change it in Cats (and this may be too drastic of a change) I think we just have to let the callback's error propagate instead.
There was a problem hiding this comment.
ahhhh, you're right. So, my first implemenaton was accidentally correct :p.
With this change, if the onError callback raises any error, We'll report that error and then re-raise the original error.
| @deprecated("Use onError with PartialFunction argument", "3.6.0") | ||
| def onError(f: Throwable => IO[Unit]): IO[A] = { | ||
| val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t) } | ||
| onError(pf) | ||
| } | ||
|
|
||
| /** | ||
| * Execute a callback on certain errors, then rethrow them. Any non matching error is rethrown | ||
| * as well. | ||
| * | ||
| * Implements `ApplicativeError.onError`. | ||
| */ | ||
| def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = | ||
| handleErrorWith(t => | ||
| pf.applyOrElse(t, (_: Throwable) => IO.unit).reportError *> IO.raiseError(t)) |
There was a problem hiding this comment.
I guess we should use reportError for the deprecated onError, but for the onError consistent with ApplicativeError we have to match the semantics defined in Cats.
| @deprecated("Use onError with PartialFunction argument", "3.6.0") | |
| def onError(f: Throwable => IO[Unit]): IO[A] = { | |
| val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t) } | |
| onError(pf) | |
| } | |
| /** | |
| * Execute a callback on certain errors, then rethrow them. Any non matching error is rethrown | |
| * as well. | |
| * | |
| * Implements `ApplicativeError.onError`. | |
| */ | |
| def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = | |
| handleErrorWith(t => | |
| pf.applyOrElse(t, (_: Throwable) => IO.unit).reportError *> IO.raiseError(t)) | |
| @deprecated("Use onError with PartialFunction argument", "3.6.0") | |
| def onError(f: Throwable => IO[Unit]): IO[A] = { | |
| val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t).reportError } | |
| onError(pf) | |
| } | |
| /** | |
| * Execute a callback on certain errors, then rethrow them. Any non matching error is rethrown | |
| * as well. | |
| * | |
| * Implements `ApplicativeError.onError`. | |
| */ | |
| def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = | |
| handleErrorWith(t => | |
| pf.applyOrElse(t, (_: Throwable) => IO.unit)*> IO.raiseError(t)) |
For the ApplicativeError.onError method, We let the callback's error propagate to match with cats's ApplicativeError semantic. For the old onError, We report callback's error and raise the original error instead.
armanbilge
left a comment
There was a problem hiding this comment.
Nice, this looks good to me. It turns out the old onError was inconsistent both in signature and in behavior, so makes sense to deprecate.
|
@lenguyenthanh if you want a small follow-up PR, I think it would be worthwhile to add a law to Cats testing this behavior when |
|
thanks @armanbilge for your guidance, It helped me a lot! For the Not sure why We didn't catch it here, maybe because of the generator? |
@lenguyenthanh ah yes, you're right. Even though there is no explicit test for this case, it should be covered by the generator.
I was confused about this as well, but I think I know why. We actually need to do one more thing in your PR: we need to add an Actually, we should make sure that every custom method on |
@armanbilge added the |
Hmm, if I restore the semantics mismatch/bug, then the laws fail. So seems to be working. diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala
index ee3a9da1f..408b2a5e9 100644
--- a/core/shared/src/main/scala/cats/effect/IO.scala
+++ b/core/shared/src/main/scala/cats/effect/IO.scala
@@ -596,7 +596,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
* Implements `ApplicativeError.onError`.
*/
def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] =
- handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t))
+ handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit).voidError *> IO.raiseError(t))
/**
* Like `Parallel.parProductL` |
|
|
||
| def onError(f: Throwable => IO[Unit]): IO[A] = | ||
| handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) | ||
| @deprecated("Use onError with PartialFunction argument", "3.6.0") |
There was a problem hiding this comment.
Hmm, this deprecation might be a bit confusing, because lambda syntax foo => bar is a valid partial function but we can't use it with onError since it picks the deprecated overload.
There was a problem hiding this comment.
Yes, that was an oversight ...
This also deprecates the old
IO.onErrorfunction and replace it's usage with the new one.Close #4058