Tracking Issue for `OnceCell/Lock::try_insert()`
Opened this issue · 7 comments
Feature gate: #![feature(once_cell_try_insert)]
This is a tracking issue for OnceCell::try_insert() and OnceLock::try_insert().
This adds a method similarly to OnceCell/Lock::set() but returns a reference at the same time. This is also similar to OnceCell/Lock::get_or_init() but the return value also tells you if the value was actually inserted or if the OnceCell/Lock was already occupied.
Public API
impl<T> OnceCell<T> {
pub fn try_insert(&self, value: T) -> Result<&T, (&T, T)>;
}Steps / History
- ACP: rust-lang/libs-team#276
- Implementation: #116540
- Final comment period (FCP)1
- Stabilization PR
Unresolved Questions
- None yet.
Footnotes
Should the return type be Result<&T, (&T, T)> or (&T, Option<T>) or (&T, Result<T, ()>)? It is easier to use the (&T, Option<T>) version but it does not encode the fact that there was a failure to insert ((&T, Result<T, ()>) encodes this better, but still not as good as Result<&T, (&T, T)>). On the other hand, the differences are not that significant since one is likely to match or the result of try_insert() anyway, since for try_insert().unwrap() we have the better set().unwrap().
I'm working on #121641 and wonder if we should add &mut both in the receiver and the result. Generally insert is a mutation and we can get a mut ref for the result if succeed.
Is there a reason, why this takes a value directly instead of an FnOnce like get_or_init and friends do?
I would like to use this in a case where I have to know after a get_or_init if the call inserted a value or not, but with the current design it seems that I have to check with get first to avoid creating a value that's not actually needed.
I'm unsure how I missed these comments but here goes:
Should the return type be
Result<&T, (&T, T)>or(&T, Option<T>)or(&T, Result<T, ()>)? It is easier to use the(&T, Option<T>)version but it does not encode the fact that there was a failure to insert ((&T, Result<T, ()>)encodes this better, but still not as good asResult<&T, (&T, T)>). On the other hand, the differences are not that significant since one is likely tomatchor the result oftry_insert()anyway, since fortry_insert().unwrap()we have the betterset().unwrap().
Given that all these options fulfill the requirements, personally I'm still in favor of Result<&T, (&T, T)> because it seems to be the easiest to use.
I'm working on #121641 and wonder if we should add
&mutboth in the receiver and the result. Generally insert is a mutation and we can get a mut ref for the result if succeed.
I'm assuming you mean adding new &mut APIs, because altering the ones proposed here would defeat its purpose.
The only use case I know of is only valid if the value is shared, otherwise get_or_try_init() would cover it. So I'm unsure if a &mut API addition would bring any value to the table in this case, seeing that get_mut_or_try_init() exists.
Is there a reason, why this takes a value directly instead of an
FnOncelikeget_or_initand friends do?I would like to use this in a case where I have to know after a
get_or_initif the call inserted a value or not, but with the current design it seems that I have to check withgetfirst to avoid creating a value that's not actually needed.
That sounds quite reasonable to me, I will make a PR proposing this change when I get to it.
Is there a reason, why this takes a value directly instead of an
FnOncelikeget_or_initand friends do?
I would like to use this in a case where I have to know after aget_or_initif the call inserted a value or not, but with the current design it seems that I have to check withgetfirst to avoid creating a value that's not actually needed.That sounds quite reasonable to me, I will make a PR proposing this change when I get to it.
While implementing it I realized that this is a bit different.
This API is more similar to set(), which takes a value as well. Taking a function would have different tradeoffs, e.g. any values owned by this function would be lost, users would have to use workarounds like this:
let mut value = Some(value);
match self.try_insert(|| value.take().unwrap()) {
Ok(value) => do_your_thing(),
Err(old_value) => {
let new_value = value.unwrap();
do_your_other_thing();
}
}So I think this would need another API addition.
@Zollerboy1 wrote:
I would like to use this in a case where I have to know after a
get_or_initif the call inserted a value or not, but with the current design it seems that I have to check withgetfirst to avoid creating a value that's not actually needed.
For that use case you can use this pattern:
let mut new = false;
let reference = cell.get_or_init(|| {
new = true;
1
});
if new {}
If the initalizer terminates successfully, the newly generated value will always be used to initialize the cell. And since get_or_init propagates panics, an unsuccessful termination will not reach if new.
@daxpedda wrote:
Given that all these options fulfill the requirements, personally I'm still in favor of Result<&T, (&T, T)> because it seems to be the easiest to use.
Can you show a motivating example where this API is easiest to use? The example you showed in the ACP would look simpler using the (&T, Option<T>) or (&T, Result<(), T>) variants:
let (value, my_value) = VALUE.try_insert(calculate_value());
println!("report value: {value:?}");
if let Err(my_value) = my_value {
assert_eq!(value, &my_value, "somehow calculated values differed");
}
Given that all these options fulfill the requirements, personally I'm still in favor of Result<&T, (&T, T)> because it seems to be the easiest to use.
Can you show a motivating example where this API is easiest to use? The example you showed in the ACP would look simpler using the
(&T, Option<T>)or(&T, Result<(), T>)variants:let (value, my_value) = VALUE.try_insert(calculate_value()); println!("report value: {value:?}"); if let Err(my_value) = my_value { assert_eq!(value, &my_value, "somehow calculated values differed"); }
match VALUE.try_insert(calculate_value()) {
Ok(value) => println!("report value: {value:?}"),
Err(old_value, new_value) => assert_eq!(old_value, new_value, "somehow calculated values differed"),
}I still find Result<&T, (&T, T)> simpler, I guess its subjective.
In a way (&T, Option<T>) or (&T, Result<(), T>) would be simpler if you want to ignore the Option/Result, but simpler APIs exist in this case. So it seems to me that returning a Result upfront is the most appropriate return type for this API.
I'm sure more qualified Rustaceans are able to more objectively assess the pros and cons here.