-
Notifications
You must be signed in to change notification settings - Fork 557
Remove Ref
's flatModify
.
#3311
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
Remove Ref
's flatModify
.
#3311
Conversation
…ffect will be run.
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.
Thank you!
@@ -103,7 +103,10 @@ abstract class Ref[F[_], A] extends RefSource[F, A] with RefSink[F, A] { | |||
/** | |||
* Like `modify` but the evaluation of the return value is wrapped in the effect type `F`. | |||
*/ | |||
def flatModify[B](f: A => (A, F[B]))(implicit F: FlatMap[F]): F[B] = F.flatten(modify(f)) | |||
def flatModify[B, E](f: A => (A, F[B]))(implicit F: MonadCancel[F, E]): F[B] = |
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 have to admit that when I've seen the initial issue (#3306) I didn't understand it immediately and it took me a minute to wrap my head around this problem. Maybe it would be nice to put a comment explaining what we're doing here and why (@armanbilge's remark about "the border between completing the modify
and beginning the effect" helped). 🙂
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 me this was more about the requirement that all operations on Ref
need to be atomic. I guess this comment would be helpful to explain how this particular implementation is achieving that. @armanbilge let me know if you'd like me to add it.
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 had to fix the formatting anyway, so I took the opportunity to add the additional comments too. :-)
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.
Thank you so much, looks great! 🙂
The one CI failure I see here, mentioning |
I see CI is all clean now 😀 |
I'm not sure "ensure atomicity" is the correct terminology here. At best, it's misleading. What this does is simply that the resulting |
Shall we remove ambiguity in the comment, such as below?
|
I looked into this more and I think I was wrong about how cancelation works. Unfortunately that means this PR doesn't address the issue. Basically the property we are relying on is that: IO.uncancelable(poll => ioa *> poll(iob.uncancelable)) <-> (ioa *> iob).uncancelable Essentially, we are relying on a guarantee that there will be no cancelation between Unfortunately, that is not the case. Here's a demo program that shows it is still possible to be canceled between //> using lib "org.typelevel::cats-effect::3.4.2"
import cats.effect._
object App extends IOApp.Simple {
// question: is this equivalent to `(ioa *> iob).uncancelable` ?
def uncancelableProductR(ioa: IO[Unit], iob: IO[Unit]): IO[Unit] =
IO.uncancelable(poll => ioa *> poll(iob.uncancelable))
def count = IO.ref(0).flatMap { counter =>
val addTwo = uncancelableProductR(counter.update(_ + 1), counter.update(_ + 1))
addTwo.start.flatMap { fiber =>
fiber.cancel *> counter.get
}
}
def run =
(count <* IO.cede)
// either the fiber is canceled before it starts, or it runs to completion. right?
.iterateWhile(result => result == 0 || result == 2)
.flatMap { unexpectedResult =>
IO.println(s"got $unexpectedResult")
}
} output:
In retrospect it makes sense I suppose. I'm a little disappointed that this is how it works, since it means the responsibility for managing uncancelability falls to users in cases like this. I wonder if it's possible and useful to change this semantic, or too harmful. |
That is very interesting, @armanbilge. Aside from changing the semantic, do I understand correctly that our immediate options are:
|
@mn98 right, that's a good summary. There is sort of a third option, but it would be highly unusual. We could define something like this: def flatModify[B, E](f: Poll[F] => A => (A, F[B]))(implicit F: MonadCancel[F, E]) This would be an extension of option (2): we make the operation uncancelable and pass the But for now, probably the way forward is improving the documentation as proposed in (1). Maybe this is not unreasonable anyway: the user would still have to remember to make their I think there's a lot places that may have this issue (recently I was looking at |
flatModify
is an atomic operation.Ref
's flatModify
.
I've made a change to this effect and updated the PR's title accordingly. It all feels a bit underwhelming now but it's been enlightening nevertheless! :-) |
Heh, sorry for sending you on that wild goose chase 😅 thanks for all your work on this! If you are looking for a more interesting follow-up—as I mentioned above there are at least a few places in FS2 (and possibly other projects) where we are using |
Fabio, Daniel, and I discussed this PR earlier on Discord today. My current summary opinion is that:
|
I caught up on the Discord discussion. Is it preferable to remove this combinator until a decision is taken on the best signature for exposing |
@armanbilge I agree on both points. (I was surprised |
@durban, |
Well, actually I think this is exactly why it's worth: there are some not-so-obvious cancelation issues to think about, which is precisely why having a helpful combinator would bring these to the forefront. The tricky bit of course is what such an API should look like, and Fabio makes a good point about having a select few, well-known region operators. Basically, by modifying a
Tangentially, I find this design pattern of "assuming responsibility" interesting. Compare with a more Actor-like pattern, where some other "supervising" entity has full responsibility for this state and running related effects. In this case, even if the external entity initiating the modification is canceled, this does not corrupt the state machine because it is "owned" by its supervisor. |
@mn98 Sure, that sounds perfectly reasonable, I didn't mean anything by "I was surprised". @armanbilge I think you convinced me. Cancellation is hard; if there is a way of doing this which helps in it, that is a good thing. (As you've discovered, as it is now, it doesn't really help; it may even harm.)
Splitting hairs, but this has its limits: even if I think I've woken up a fiber, it might get cancelled before actually waking up, so I can't be sure (e.g., queues and "lost" items). But that is arguably a bug in that fiber. So in general, I agree. |
@durban thanks, great you agree :) now just to figure out how 😅
Yes it does, and I am interested in plugging more of these holes. For example see #3233. |
I just randomly remembered: other APIs where we pass the Edit: this could even suggest an API where |
…ing to cancelability.
Ref
's flatModify
.Ref
's flatModify
.
After much debate, both here and on the TypeLevel Discord, it's been decided that this combinator should be removed entirely, owing to the semantics around cancellation. As a separate follow up, as suggested by @SystemFw, we may want to introduce an improved version with a |
I would be in favor of this, with an uncancelable |
Oops, wrong button! |
An uncancelable Do you want this PR to become that again, or should we go ahead and merge this one to remove the combinator before re-opening that debate? |
Well, not quite: it would be completely uncancelable, so it would not wrap the user effect in
I added this PR to the v3.5.0 milestone so we won't forget to resolve this before the release. Basically, let's see what Daniel thinks ;) thanks for all your work and patience! |
Ok, makes sense.
No problem at all, contributing in some small capacity is a great way to learn :-) |
@mn98 Thanks for sticking with this! You've honestly done some really good work here. |
If the
Ref
updates then the effect should be run.