Skip to content

Tracking issue for Cell::update #50186

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

Closed
ghost opened this issue Apr 23, 2018 · 70 comments · Fixed by #134446
Closed

Tracking issue for Cell::update #50186

ghost opened this issue Apr 23, 2018 · 70 comments · Fixed by #134446
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. Libs-Small Libs issues that are considered "small" or self-contained Libs-Tracked Libs issues that are tracked on the team's project board. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@ghost
Copy link

ghost commented Apr 23, 2018

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 do c.update(|x| x + 1):

let c = Cell::new(5);
let new = c.update(|x| x + 1);

assert_eq!(new, 6);
assert_eq!(c.get(), 6);
@ghost ghost mentioned this issue Apr 23, 2018
@sfackler sfackler added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. B-unstable Blocker: Implemented in the nightly compiler and unstable. labels Apr 24, 2018
@oli-obk
Copy link
Contributor

oli-obk commented Apr 24, 2018

Not a blocking issue for merging this as an unstable feature, so I'm registering my concern here:

The update function returning its new value feels very ++i-ish. Similar to += returning the result of the operation (which rust luckily does not!). I don't think we should be doing this. Options I see:

  • return () (that was the suggestion in the PR)
  • make the closure argument a &mut T, and allow the closure to return any value.
    • if users want the "return new/old value" behaviour, they can now encode it themselves via |x| {*x += 1; *y }

Maybe there are other options?

Data point: the volatile crate's update method returns () (https://github.com/embed-rs/volatile/blob/master/src/lib.rs#L134)

@ghost
Copy link
Author

ghost commented Apr 24, 2018

@oli-obk

I like the second option (taking a &mut T in the closure). There's an interesting variation on this option - instead of requiring T: Copy we could require T: Default to support updates like this one:

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);

@ghost ghost mentioned this issue May 2, 2018
@ghost
Copy link
Author

ghost commented May 2, 2018

What do you think, @SimonSapin? Would two such methods, get_update and take_update, be a more appropriate interface?

@SimonSapin
Copy link
Contributor

The T: Default flavor feels kinda awkward but I don’t know how to put that in more concrete terms, sorry :/

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.

bors added a commit that referenced this issue Jul 23, 2018
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.
@CodeSandwich
Copy link

CodeSandwich commented Aug 6, 2018

Taking &mut in this method would be awesome, I would love to use it.

Unfortunately it seems that it would have to be unsafe, because AFAIK there is no way in Rust to restrict usage like this:

let c = Cell::new(vec![123]);
c.update(|vec| {
    let x = &mut vec[0];            // x references 123
    c.update(|vec| vec.clear());    // x references uninitialised memory
    *x = 456;                       // BOOM! undefined behavior
})

I hope, I'm wrong.

@SimonSapin
Copy link
Contributor

@CodeSandwich it is a fundamental restriction of Cell<T> that a reference &T or &mut T (or anything else inside of T) must not be given to the value inside the cell. It’s what makes it sound. (This is the difference with RefCell.)

We could make an API that takes a closure that takes &mut T, but that reference would not be to the inside of the cell. It would be to value that has been copied (with T: Copy) or moved (and temporarily replaced with some other value, with T: Default) onto the stack and will be copied/moved back when the closure returns. So code like your example would be arguably buggy but still sound: the inner update() would operate on a temporary value that would be overwritten at the end of the outer one.

@CodeSandwich
Copy link

CodeSandwich commented Aug 13, 2018

I think, that a modifier method might be safe if we just forbid usage of reference to Cell's self inside. Rust does not have mechanism for forbidding usage of a SPECIFIC reference, but it does have mechanism for forbidding usage of ALL references: 'static closures.

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.

@SimonSapin
Copy link
Contributor

A 'static closure could still reach the Cell, for example by owning a Rc.

@CodeSandwich
Copy link

That's absolutely right :(

@XAMPPRocky XAMPPRocky added the C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC label Oct 2, 2018
@earthengine
Copy link

earthengine commented Oct 3, 2018

It seems like, the semantic are a bit different between Default and Copy: the later is more light weighted, as it does not have to fill the hole behind with some temporary rubbish.

What shall we name those variants? update_move for the Default case?

Also there is another variation: update_unsafe which will be unsafe and does not require a trait bound: it only moves the value and leave uninitialized value behind, before the method returns. This will make any nested calls UB and so have to be marked as unsafe.

pub unsafe fn update_unsafe<F>(&self, f: F) -> T where F: FnOnce(T) -> T;

@SimonSapin
Copy link
Contributor

if you’re gonna use unsafe anyway you can use .get() and manipulate the raw pointer. I don’t think we should facilitate it more than that.

@yuriks
Copy link
Contributor

yuriks commented Oct 15, 2018

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 update, get_and_update and update_and_get:

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 Cell.

@peterjoel
Copy link
Contributor

Related conversation on a similar PR (for RefCell): #38741

@matthew-mcallister
Copy link
Contributor

matthew-mcallister commented Jan 6, 2020

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 ++i or i++ behavior out of it. Also it is the same whether T: Copy or not. Though ergonomics are of course debatable.

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

@Lucretiel
Copy link
Contributor

Lucretiel commented Jan 17, 2020

+1 for a T version, as opposed to &mut T. It's easy to construct demo code that is more expressive with either version (ie, x + 1 vs vec.push(..)), but unless we add both, I think the more general version is preferable, since under the hood they both require a swap-out swap-in for soundness. Also +1 for an extra version with a return value.

Finally, I think Default is preferable to Copy. My gut is that it covers a wider range of cases; that is, it's more likely for a type to implement Default but not Copy than the other way around, and hopefully for simple cases the compiler should be able to optimize out the extra writes anyway.

@camsteffen
Copy link
Contributor

It sounds like accepting &mut T isn't a viable option.

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);

@m-ou-se
Copy link
Member

m-ou-se commented Feb 29, 2020

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;

[Demo implementation]

@fogti
Copy link
Contributor

fogti commented Mar 2, 2020

@m-ou-se I think that would be unsafe suprising because multiple instances of the 'smart pointer' / guard could exist simultaneously.

        let mut cu1 = b.copy_update();
        let mut cu2 = b.copy_update();
        *cu1 += 1;
        *cu2 += 1;
        *cu1 += *cu2;

https://play.rust-lang.org/?version=beta&mode=debug&edition=2018&gist=f3fc67425f1b7f1dba1220fb961ad7d9

@Lucretiel
Copy link
Contributor

Lucretiel commented Mar 2, 2020

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)

@m-ou-se
Copy link
Member

m-ou-se commented Mar 2, 2020

None of the proposed solutions (with a T -> T lambda, with a &mut T -> () lambda, or with a 'smart pointer'/RaiiGuard/younameit, etc.) are unsafe. All of them can be implemented using only safe code.

All of them have this 'update while updating' problem. That's not inherent to a specific solution, but inherent to Cell. There's just no way to 'lock' access to a Cell. That's the entire point of a Cell. Rust has a built-in solution for this problem in general: only allowing mutation through a provably uniquely borrowed &mut. Cell is the way to explicitly disable that feature.

With the current (unstable) update() function: [Demo]

    let a = Cell::new(0);
    a.update(|x| {
        a.update(|x| x + 1);
        x + 1
    });
    assert_eq!(a.get(), 1); // not 2

With a &mut:

    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.

@BurntSushi
Copy link
Member

BurntSushi commented Jun 28, 2024

"it's different from replace because it's different"

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 _with. It's just what we usually use to disambiguate it with routines that take a value directly instead of a closure. But if taking a value directly isn't a sensible operation on its own, I don't see a problem with dropping the _with suffix.

The main issue here, from what I can tell, is the similarity between update and replace_with. But my point is that while they are superficially similar, for Cell (and Copy types specifically), the usage pattern is quite different. Different to the point that a Cell::update that doesn't take a closure probably doesn't make sense.

That is assuming that there has to be some update method, and that having one is more important than consistency with other APIs.

Sure... If we don't add update then there's no point in debating its name.

@danielhenrymantilla
Copy link
Contributor

danielhenrymantilla commented Jun 28, 2024

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 might not be 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 ().

@peterjoel
Copy link
Contributor

peterjoel commented Jun 28, 2024 via email

@camsteffen
Copy link
Contributor

camsteffen commented Jun 28, 2024

That is assuming that there has to be some update method, and that having one is more important than consistency with other APIs.

Sure... If we don't add update then there's no point in debating its name.

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".

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this PR / Issue. to-announce Announce this issue on triage meeting and removed final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. labels Jun 30, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Jun 30, 2024

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.

@apiraino apiraino removed the to-announce Announce this issue on triage meeting label Jul 4, 2024
@Pantamis
Copy link

Pantamis commented Aug 8, 2024

I also think having such method only for copy types makes sense.

For the T : Default case I wonder if it would be best just adding a method which could be named take_inspect and calls a function only on a shared reference to a temporary moved out value from the Cell:

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 Cell prevents in general). Nested calls can still be confusing but at least not UB as in case of acting on a exclusive reference. A shared ref is already very useful when it holds a handler (to a connection or thread pool) that can never be cloned, needs only a shared reference to send items or look at cell state while still having the ability to change the contained value in some rare occasion without having to use a RefCell.

Is there a RFC proposing this idea already ?

tgross35 added a commit to tgross35/rust that referenced this issue Dec 18, 2024
tgross35 added a commit to tgross35/rust that referenced this issue Dec 18, 2024
tgross35 added a commit to tgross35/rust that referenced this issue Apr 2, 2025
tgross35 added a commit to tgross35/rust that referenced this issue Apr 2, 2025
Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
@tgross35
Copy link
Contributor

tgross35 commented Apr 2, 2025

libs-api discussed this again and decided that update should have no return value. API update at #139273.

@tgross35
Copy link
Contributor

tgross35 commented Apr 2, 2025

I can't update the top post because it was created by a deleted user, but the API will be:

impl<T: Copy> Cell<T> {
    pub fn update(&self, f: impl FnOnce(T) -> T);
}

m-ou-se added a commit to m-ou-se/rust that referenced this issue Apr 3, 2025
…pratt

Apply requested API changes to `cell_update`

Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Apr 3, 2025
…pratt

Apply requested API changes to `cell_update`

Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Apr 3, 2025
Rollup merge of rust-lang#139273 - tgross35:cell-update-changes, r=jhpratt

Apply requested API changes to `cell_update`

Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
@tgross35
Copy link
Contributor

tgross35 commented Apr 7, 2025

Crosslinking, FCP for the changed API is currently ongoing at #134446 (comment)

github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this issue Apr 8, 2025
Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this issue Apr 8, 2025
…pratt

Apply requested API changes to `cell_update`

Do the following:

* Switch to `impl FnOnce` rather than a generic `F`.
* Change `update` to return nothing.

This was discussed at a libs-api meeting [1].

Tracking issue: rust-lang#50186

[1]: rust-lang#134446 (comment)
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Apr 24, 2025
…jhpratt

Stabilize the `cell_update` feature

Included API:

```rust
impl<T: Copy> Cell<T> {
    pub fn update(&self, f: impl FnOnce(T) -> T);
}
```

FCP completed once at rust-lang#50186 (comment) but the signature has since changed.

Closes: rust-lang#50186
@bors bors closed this as completed in ac34a6f Apr 24, 2025
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Apr 24, 2025
Rollup merge of rust-lang#134446 - tgross35:stabilize-cell_update, r=jhpratt

Stabilize the `cell_update` feature

Included API:

```rust
impl<T: Copy> Cell<T> {
    pub fn update(&self, f: impl FnOnce(T) -> T);
}
```

FCP completed once at rust-lang#50186 (comment) but the signature has since changed.

Closes: rust-lang#50186
github-actions bot pushed a commit to rust-lang/miri that referenced this issue Apr 25, 2025
Included API:

    impl<T: Copy> Cell<T> {
        pub fn update(&self, f: impl FnOnce(T) -> T);
    }

Closes: rust-lang/rust#50186
github-actions bot pushed a commit to rust-lang/miri that referenced this issue Apr 25, 2025
Stabilize the `cell_update` feature

Included API:

```rust
impl<T: Copy> Cell<T> {
    pub fn update(&self, f: impl FnOnce(T) -> T);
}
```

FCP completed once at rust-lang/rust#50186 (comment) but the signature has since changed.

Closes: rust-lang/rust#50186
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. Libs-Small Libs issues that are considered "small" or self-contained Libs-Tracked Libs issues that are tracked on the team's project board. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.