-
Notifications
You must be signed in to change notification settings - Fork 517
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
Make IO#onError
consistent with ApplicativeError
#4121
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
@@ -588,9 +589,19 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { | |||
def onCancel(fin: IO[Unit]): IO[A] = | |||
IO.OnCancel(this, fin) | |||
|
|||
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what we should do is extract this into a helper method.
IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
reportError
method 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)) |
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.
fixed in: 868451b
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.
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.
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`
|
This also deprecates the old
IO.onError
function and replace it's usage with the new one.Close #4058