Skip to content
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

OnceCell mut API extension #194

Closed
danielSanchezQ opened this issue Sep 5, 2022 · 6 comments
Closed

OnceCell mut API extension #194

danielSanchezQ opened this issue Sep 5, 2022 · 6 comments

Comments

@danielSanchezQ
Copy link
Contributor

danielSanchezQ commented Sep 5, 2022

Motivation:

I've experience that sometimes you want to init a type and get a mutable reference to it. Right now the way of doing that is to use use any of the available API calls that can initialise the value, depending on the needs:

  • get_or_init
  • get_or_try_init
  • try_insert

Then, discard the &T returned and use get_mut to get a &mut T. For example:

let mut cell: OnceCell<u32> = OnceCell::new();
_ = cell.get_or_init(|| 10);
let v = cell.get_mut().unwrap_or_else(|| unreachable!());
*v += 1;

Playground

let v = cell.get_mut().unwrap_or_else(|| unreachable!()); looks like a code smell.

This, even if working, breaks a couple of things:

  • It is not intuitive, almost every API in the std counts with its mut counterpart (see slices or vec)
  • forces to write extra calls where we could just return a &mut T at the time we need

Proposition:

Implement mut versions of the API calls already implemented. To get something like:

let mut cell: OnceCell<u32> = OnceCell::new();
let v = cell.get_mut_or_init(|| 10);
*v += 1;
@danielSanchezQ danielSanchezQ changed the title OnceCell mut API OnceCell mut API extension Sep 5, 2022
@danielSanchezQ
Copy link
Contributor Author

This was once implemented as part of #193 , but was removed. I keep a branch with it implemented which we could bring back and work with again.

@matklad
Copy link
Owner

matklad commented Sep 5, 2022

My gut feeling is that this is a rather narrow use-case, and that it isn't worth to duplicate many API functions. The .set(...); .get_mut().unwrap() workaround seems OK enough.

If we do want to add anything here, it would be, perhaps,

try_insert_mut(&mut self, value: T) -> Result<&mut T, (&mut T, T)>

as try_insert is intended as the most general API to use when one needs control. For _or_inig variants specifically, I don't think they are worth is especially because with &mut the caller can write the if cell.get().is_none() themselves.

@danielSanchezQ
Copy link
Contributor Author

My gut feeling is that this is a rather narrow use-case, and that it isn't worth to duplicate many API functions. The .set(...); .get_mut().unwrap() workaround seems OK enough.

Using set force you to build the value each time instead of being something lazy.

If we do want to add anything here, it would be, perhaps,

try_insert_mut(&mut self, value: T) -> Result<&mut T, (&mut T, T)>

as try_insert is intended as the most general API to use when one needs control.

Well, better this than nothing yes.

For _or_inig variants specifically, I don't think they are worth is especially because with &mut the caller can write the if cell.get().is_none() themselves.

I don't really understand this 😅

@matklad
Copy link
Owner

matklad commented Sep 5, 2022

I don't really understand this sweat_smile

So, the reason why we need .get_or_init() is because the following code:

let cell: &OnceCell<T> = ...;
let r: &T = match cell.get() {
    Some(it) => it,
    None => {
        cell.set(f());
        cell.get().unwrap()
    }
};

would be racy -- the .set() might not actually set the value, because it might be filled by some different thread. The API with closure is needed to ensure atomicity, that the closure is only called once.

However, if we have &mut OnceCell, than the above code will be correct -- exclusive reference guarantees that there are no other threads that can sneakily set the value.

@danielSanchezQ
Copy link
Contributor Author

danielSanchezQ commented Sep 5, 2022

Oh, ok, I get it now. Imo it doesn't do harm to have them. But I understand that keeping simple is a good choice too.
Maybe we could go with try_insert_mut(&mut self, value: T) -> Result<&mut T, (&mut T, T)> as you commented, which would be enough for the other cases without adding the other extra methods. Your call @matklad !
And thanks for taking the time to explain everything!!

@matklad
Copy link
Owner

matklad commented Oct 22, 2022

I think I am inclined to not add these for now!

@matklad matklad closed this as completed Oct 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants