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 T version, as opposed to 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. |
Then I suppose the behavior of nested updates needs to at least be well defined. I would think that the update method opens the door for compiler optimizations, and that could be part of the motivation for adding the method. For example, shouldn't we be able to optimize away |
With all proposed solutions, 'nested' updates are well defined. It isn't needed for optimization. The optimizer will understand a .get()+.set() (or .take()+.set()) just as well. An update function is just to make it shorter to write, and possibly to make it easier to write correct code. In most cases, the optimizer will be able optimize the [Demo] |
The "smart pointer" idea is really neat. It just seems a little too obfuscated since the write back to the |
I second the motion that we replace The smart pointer idea could be considered as a separate enhancement, unless people feel that that should be the main implementation of |
By definition, a mutable borrow is an exclusive borrow. |
Can't we require closure to be fn update<F>(&self, f: F) where F: Send + FnOnce(&mut T); Since |
This is an abuse of |
@rrevenantt I brought up the same idea several years ago; unfortunately it's still not sufficient for soundness. The closure could still refer to a @RustyYato Please try to keep an open mind. I'm sure there have been other cases in Rust's history where something originally intended for one purpose turned out to be useful for another one as well, and in general it can be a fruitful and enlightening direction to explore. This also would not have removed any of
(And furthermore, if you are using a |
Any reason why the API for RefCell and Cell is so inconsistent? RefCell has a method replace_with similar to the method update for Cell but return the old value instead. Shouldn't it be named update_with and be available on both or at least have the methods update and replace_with available for both? |
Today I’m writing a helper method (local to my crate) that does the same as There are a few possible variations of the API, but at this point more time on Nightly is not gonna bring much new information about which is better.
It’s not just similar, an exclusive borrow is exactly what Exclusive borrows don’t help with this issue, though. The whole point of |
Regarding naming, what about Also, the fact that we get a let counter: Cell<i32> = Cell::new(0);
counter.set_with(|c: i32| {
counter.set_with(|c| c + 1);
c + 1 // <- clearer that this is the *original* value + 1
});
assert_eq!(counter.get(), 1); The only drawback of this approach is that since it uses the return slot for the update, extracting an arbitrary value from the closure is a bit more cumbersome. I think this is an acceptable tradeoff, given the name Finally, regarding those who like the ergonomics of a TL,DRIf C++ is any example, the footgun that a |
The 'copy vs take' issue was discussed in the library api team meeting recently. We concluded that having this functionality for only for We'd like to explore the possibility of having a single function that works on both |
Oh yeah, that would be wonderful, and I agree that |
So, just to see what that might look like... /// Automatically implemented for types that implement either `Copy` or `Default`
trait CopyOrDefault {
/// Copies the value if `Self: Copy`, otherwise replaces with `default()` and returns the previous value
fn copy_or_take(&mut self) -> Self;
}
impl Cell<T> {
fn update(&self, f: impl Fn(T) -> T)
where T: CopyOrDefault {..}
} |
Here's an implementation of this idea: #89357 |
@camsteffen The |
I kinda like the "unstable trickery" rather than exposing a new marker trait that seems really niche. We could always expose the marker trait in the future if wanted. |
But it does commit Rust to having a form of specialization capable of expressing "A or B" for unrelated A and B… are there any other standard library APIs that do this? |
I opened a related discussion at https://internals.rust-lang.org/t/pre-rfc-assign-copy-types-to-cell-without-set/16081 - maybe we can enable the assignment operator for |
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 |
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: