-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Tracking issue for Cell::update #50186
Comments
Not a blocking issue for merging this as an unstable feature, so I'm registering my concern here: The
Maybe there are other options? Data point: the |
I like the second option (taking a let c = Cell::new(Vec::new());
c.update(|v| v.push("foo")); Perhaps we could have two methods with different names that clearly reflect how they internally work? fn get_update<F>(&self, f: F) where T: Copy, F: FnMut(&mut T);
fn take_update<F>(&self, f: F) where T: Default, F: FnMut(&mut T); |
What do you think, @SimonSapin? Would two such methods, |
The It looks like there is a number of slightly different possible APIs for this. I don’t have a strong opinion on which is (or are) preferable. |
Implement rfc 1789: Conversions from `&mut T` to `&Cell<T>` I'm surprised that RFC 1789 has not been implemented for several months. Tracking issue: #43038 Please note: when I was writing tests for `&Cell<[i32]>`, I found it is not easy to get the length of the contained slice. So I designed a `get_with` method which might be useful for similar cases. This method is not designed in the RFC, and it certainly needs to be reviewed by core team. I think it has some connections with `Cell::update` #50186 , which is also in design phase.
Taking Unfortunately it seems that it would have to be
I hope, I'm wrong. |
@CodeSandwich it is a fundamental restriction of We could make an API that takes a closure that takes |
I think, that a modifier method might be safe if we just forbid usage of reference to I've created a simple implementation of such method and I couldn't find any hole in it. It's a limited solution, but it's enough for many use cases including simple number and collections modifications. |
A |
That's absolutely right :( |
It seems like, the semantic are a bit different between What shall we name those variants? Also there is another variation: pub unsafe fn update_unsafe<F>(&self, f: F) -> T where F: FnOnce(T) -> T; |
if you’re gonna use |
IMO, closure receiving a &mut to a stack copy isn't that advantageous ergonomically in terms of offering flexibility. You still need to jump through hoops to get the old value if you want to. Consider: let id = next_id.update(|x| { let y = *x; *x += 1; y }); I think the most universally applicable thing to do would be to offer all of things.update(|v| v.push(4)); // returns ()
// Alternative with v by-move. I'd guess this one isn't as useful, and you can easily use
// mem::swap or mem::replace in the by-ref version instead if you need it.
things.update(|v| { v.push(4); v });
let id = next_id.get_and_update(|x| x + 1);
let now = current_time.update_and_get(|x| x + 50); These are 3 additional names, but I think they'll make the function more useful. A by-mut-ref update would only be useful for complex types where you're likely to do in-place updates, but I find myself doing the .get() -> update/keep value -> .set() pattern a lot with |
Related conversation on a similar PR (for |
Has it been considered to use an arbitrary return type on top of updating the value? That is (naming aside), fn update1<F>(&self, f: F) where F: FnOnce(T) -> T;
fn update2<F, R>(&self, f: F) -> R where F: FnOnce(T) -> (T, R); The second method has a very functional feel to it and appears both flexible and sound (i.e. you can't return &T). You could get either EDIT: Something to chew on: fn dup<T: Copy>(x: T) -> (T, T) { (x, x) }
let i = i.update2(|i| (i + 1, i)); // i++
let i = i.update2(|i| dup(i + 1)); // ++i |
+1 for a Finally, I think |
It sounds like accepting I'm sort of combining previous ideas here. How about this? fn update<F>(&self, f: F) where T: Copy, F: FnOnce(T) -> T;
fn update_map<F, U>(&self, f: F) -> U where T: Copy, F: FnOnce(T) -> (T, U);
fn take_update<F>(&self, f: F) where T: Default, F: FnOnce(T) -> T;
fn take_update_map<F, U>(&self, f: F) -> U where T: Default, F: FnOnce(T) -> (T, U); |
A completely different option could be to not take a function as argument, but return a 'smart pointer' object that contains the taken/copied value and writes it back to the cell when it is destructed. Then you could write a.take_update().push(1);
*b.copy_update() += 1; |
@m-ou-se I think that would be let mut cu1 = b.copy_update();
let mut cu2 = b.copy_update();
*cu1 += 1;
*cu2 += 1;
*cu1 += *cu2; |
The proposed implementation appears to move the value out of the cell and into the smart pointer; pointer in this case is a misnomer. This will lead to the old c++ auto_ptr problem: not unsound, but surprising in nontrivial use cases (ie, if two exist at the same time, one will receive a garbage value) |
None of the proposed solutions (with a All of them have this 'update while updating' problem. That's not inherent to a specific solution, but inherent to With the current (unstable) let a = Cell::new(0);
a.update(|x| {
a.update(|x| x + 1);
x + 1
});
assert_eq!(a.get(), 1); // not 2 With a let a = Cell::new(0);
a.update(|x| {
a.update(|x| *x += 1);
*x += 1;
});
assert_eq!(a.get(), 1); // not 2 With a copy-containing 'smart pointer': let a = Cell::new(0);
{
let mut x = a.copy_update();
*a.copy_update() += 1;
*x += 1;
}
assert_eq!(a.get(), 1); // not 2 There is no way to avoid this problem. Every possible solution we come up with will have this problem. All we can do is try to make it easier/less verbose to write the commonly used .get() + .set() (or .take() + .set()) combination. |
Currently, the implementation use |
@psionic12 perf-wise, a |
@danielhenrymantilla I'm sorry I forgot the "re-entrant" thing, if someone somehow needs to access the cell again in |
To add one more color:
"swap" in the name gives intuition about what happens under the hood |
So in the long term, we'd like to have Assuming we want to reserve impl Cell<T: Copy> {
// Unresolved: return T or not?
// Unresolved: return new T or old T?
pub fn get_update(&self, impl FnOnce(T) -> T) -> T { ... }
}
impl Cell<T: Default> {
pub fn take_update(&self, impl FnOnce(T) -> T) { ... }
} |
Allow updating variables outside of the scope, such as to extract the old value. This will be more consistent with `Atomic*::fetch_update`. Tracking issue: rust-lang#50186
What do you think about
Method will temporary swap 2 cells, execute callback and swap them again? |
We discussed this in the @rust-lang/libs-api meeting today and decided to stabilize The main reason is that, fundamentally, @rfcbot fcp merge |
Team member @Amanieu has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
Hmm. I'm somewhat perplexed that this issue is six years old, now in stabilization FCP, and despite a lot of bikeshedding about the name, AFAICS only one comment (which seems to have been missed/ignored) has raised the issue that "update" (and all the other proposed names in fact) is inconsistent with several other APIs, including
In addition, all of these return the old value except If this inconsistency is desired or necessary in some way, I think it should be clearly justified before stabilization. Footnotes
|
@jdahlstrom I do think the naming situation here is a little odd. The fact that I think the case in favor of So after thinking through this, while |
@BurntSushi To be clear, I find that the "update with a closure" pattern is entirely reasonable in itself; what I’m concerned about is
If (2) is desired because it permits some Cell-specific pattern then (1) is defensible because then naming the method |
I think we're saying the same thing. Improving the docs sounds good to me. I'm not quite sure what the best phrasing would be, but I don't think this particular doc update should block stabilization. |
Just playing devil's advocate if I may. I don't find myself having a strong opinion on the name.
That is assuming that there has to be some
This feels like circular reasoning to me - "it's different from replace because it's different". To me it seems like If it seems viable to return either old or new value, but neither are really needed since it's Copy, perhaps it's better to not return anything. |
That's not what I said though. A better summary of what I said is "the name is different because the usage pattern of the routine is different." That's not circular reasoning. Like, we don't have a rule in std that every single method that takes a closure has to end with the suffix The main issue here, from what I can tell, is the similarity between
Sure... If we don't add |
If
|
Daniel Henry-Mantilla wrote:
That way a future release could always decide to return a copy of T rather
than ().
I believe this would still not be backwards compatible if `T` is
`#[must_use]`.
Peter
…On Fri, 28 Jun 2024 at 16:03, Daniel Henry-Mantilla < ***@***.***> wrote:
If Cell::update() returns the old value
-
it is consistent with RefCell::replace_with();
-
but just to allow replacing:
let old = cell.get();
cell.set(…);if old … {
with:
let old = cell.update(|old| …);if old … {
which isn't that much more useful than plain set-&-get.
If Cell::update() returns the new value:
-
It allows replacing:
let old = cell.get(); // <- or inlined.let new = …;
cell.set(new);if new … {
with:
let new = cell.update(|old| …);if new … {
which, in the non-inlined cell.get() case, starts having maybe enough
value.
-
But it is inconsistent with RefCell::update_with()!
I kind of agree that, whilst not a deal breaker thanks to the
different name, it's kind of unfortunate to have this inconsistency-ish
thing, just for the sake of a tiny convenience around returning the new
value. It is probably not worth doing.
If Cell::update() does not return anything
Then we lose *some* convenience, but we retain the main counter.update(|n|
n + 1) aspect of the API.
------------------------------
Conclusion
Since the "return-old" is barely more convenient than explicit .set() and
.get(), and "return-new" is slightly inconsistent with other "similar"
APIs, returning nothing may be the most sensible for the time being.
------------------------------
Aside: a possibility to punt on the decision Click to see
would be to have the API of Cell::update() return (), but with the
specific choice of () being opaque:
impl<T: Copy> Cell<T> {
fn update<'t>(&self, f: impl FnOnce(T) -> T) -> impl 't + Sized
where
T: 't,
{
self.set(f(self.get()));
}}
That way a future release could always decide to return a copy of T
rather than ().
—
Reply to this email directly, view it on GitHub
<#50186 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABE2WY2MW4CFM5UW7Z3QELZJV3NPAVCNFSM6AAAAABI4TSL7WVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCOJXGEZTOOBZHE>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Sorry, what I mean to say is "...assuming that there needs to be a method named 'update'". I think the baseline here is that we want a closure-taking method. The name and the return value are the "negotiables". |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
I also think having such method only for copy types makes sense. For the impl<T: Default> Cell<T> {
/// Takes the contained value to call a function
/// on a reference and then moves it back in the cell.
///
/// The cell will contain `Default::default()`
/// in the function scope or if it panics.
///
/// # Examples
///
/// ```
/// use std::cell::Cell;
/// use std::sync::mpsc::channel;
///
/// let (tx, rx) = channel();
///
/// let c = Cell::new(Some(tx));
///
/// c.take_inspect(|t| t.as_ref().unwrap().send(true).unwrap());
/// c.take_inspect(|t| t.as_ref().unwrap().send(c.take().is_some()).unwrap());
///
/// println!("Received from channel: {}", rx.recv().unwrap());
/// assert_eq!(rx.recv().unwrap(), false);
/// assert_eq!(c.take().is_some(), true);
/// ```
#[inline]
pub fn take_inspect<F>(&self, f: F)
where
F: FnOnce(&T),
{
let moved = self.take();
f(&moved);
self.set(moved);
}
} Although less useful I still find nice to have a one line method to do something on a reference to the value inside a cell (which Is there a RFC proposing this idea already ? |
This issue tracks the
cell_update
feature.The feature adds
Cell::update
, which allows one to more easily modify the inner value.For example, instead of
c.set(c.get() + 1)
now you can just doc.update(|x| x + 1)
:The text was updated successfully, but these errors were encountered: