-
Notifications
You must be signed in to change notification settings - Fork 21
ACP: Option::update_or_default
#575
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
Comments
let mut opt: Option<String> = None;
opt.get_or_insert_default().push_str("Hello");
nit:
pub fn or_default_update<F>(&mut self, f: F)
where
F: FnOnce(&mut T),
T: Default, let mut opt: Option<String> = None;
opt.or_default_update(|s| s.push_str("Hello"));
You should provide some better examples that actually make use of your version passing it by value.
And if you're going to get a |
Before I want to clarify intention of this method, because it could be not clearly describe in the ACP: You're right that I very agree that Your suggestion for #[derive(Default, Debug, Clone)]
struct Config {
log_level: u8,
output_path: String,
}
impl Config {
fn merge(self, other: PartialConfig) -> Config {
Config {
log_level: other.log_level.unwrap_or(self.log_level),
output_path: other.output_path.unwrap_or(self.output_path),
}
}
}
struct PartialConfig {
log_level: Option<u8>,
output_path: Option<String>,
}
fn main() {
let mut config: Option<Config> = None;
let partial = PartialConfig {
log_level: Some(2),
output_path: None,
};
// With proposed API:
config.update_or_default(|c| c.merge(partial));
dbg!(&config); // Result: Some(Config { log_level: 2, output_path: "" })
// Current approach:
let c = config.get_or_insert_default();
*c = c.clone().merge(partial); // Clone is awkward and potentially expensive
// OR:
config = Some(config.take().unwrap_or_default().merge(partial)); // Verbose
} In a CLI application, a The transition method consumes the #[derive(Default)]
struct GameState {
score: u32,
level: u8,
}
impl GameState {
fn transition(self, event: Event) -> GameState {
match event {
Event::Score(points) => GameState {
score: self.score + points,
level: self.level,
},
Event::LevelUp => GameState {
score: self.score,
level: self.level + 1,
},
}
}
}
fn main() {
enum Event {
Score(u32),
LevelUp,
}
let mut state: Option<GameState> = None;
let event = Event::Score(100);
// With proposed API:
state.or_default_update(|s| s.transition(event));
// Result: Some(GameState { score: 100, level: 0 })
// Current approach:
state = Some(state.take().unwrap_or_default().transition(event)); // Verbose
// OR:
let s = state.get_or_insert_default();
*s = s.clone().transition(event); // Clone is inefficient
} You raised a great point about whether users want a Next StepsBased on your feedback, I propose:
|
This is related to rust-lang/rfcs#1736, you might want to see its discussion for why it is not easy to replace |
Since it returns For by-value updates, another alternative is opt.get_or_insert_default();
opt = opt.take().map(some_update_func); |
Thank you for your feedback! You're right that However, the main motivation behind |
Uh oh!
There was an error while loading. Please reload this page.
Proposal
Clear Problem Statement
The Rust
Option<T>
API lacks a concise way to atomically take ownership of the contained value, transform it, and insert a default if it wasNone
. Today, users resort to piecing together multiple methods. For example,Option::get_or_insert_default()
(andget_or_insert_with
) only returns a mutable reference to a newly-inserted default value, requiring awkward patterns when a by-value replacement is needed. Alternatively, one can calllet old = opt.take()
(which leavesNone
) and then setopt = Some(f(old.unwrap_or_default()))
, but this is verbose and error-prone. Explicitmatch
orif let
arms also require duplicating theSome(…)
construction. In short, there is no singleOption
method that cleanly expresses “ifSome(x)
applyf(x)
, else setSome(Default::default())
.”This leads to boilerplate and hidden bugs. For instance, forcing users to re-wrap values can cause logic mistakes (forgetting to reassign) or extra moves/clones of
T
. The absence of a simple update-or-initialize operation forces each crate to reimplement this common pattern in its own way, reducing code clarity.Motivating Examples
Real-world code frequently needs to update an optional value by taking ownership, or create a default if absent. In every case today the code is either verbose or indirect:
Config merging. Suppose we accumulate a configuration in an
Option<Config>
. On each update we want to merge new settings into the existingConfig
if present, otherwise start from a default. Current code must do something like:The
map_or_else(Default::default, |cfg| …)
pattern is hard to read. Withupdate_or_default
, one could write simply:which clearly means “merge into the existing config or default‐initialize it first.”
State machine transitions. Consider an
Option<State>
field in a state machine. Each tick, if there is a current state we want to replace it by the next state value; if there is no state yet, we initialize it. Currently one might write:This match is cumbersome. With
update_or_default
, it becomes:making the intent obvious.
Incremental data building. Imagine an
Option<Vec<T>>
accumulating values. On each push, do nothing ifNone
, or append toVec
ifSome
. Withoutupdate_or_default
, one commonly writes:Or equivalently:
Both forms are lengthy. With the proposed API, it would be:
which is far more concise and expressive.
These examples show that the current idioms for “update or insert default” require either matching and re-wrapping or using
take()
+map_or_else()
, both of which are verbose and can conceal intent. A dedicated method would eliminate that ceremony and reduce errors.Proposed Solution
I propose adding a method with roughly the signature:
Semantically, this does: if
self
isSome(x)
, replace it withSome(f(x))
; ifself
isNone
, insertSome(Default::default())
. In other words, take ownership of the current value (if any), apply the closure, and store the result; otherwise store the default value. Crucially, the closuref
takes its argument by value (FnOnce(T) -> T
), allowing arbitrary by-value transformations.The
FnOnce(T) -> T
approach is intentional: it lets the closure consume the oldT
without requiringT: Clone
or extra copies. (All closures implementFnOnce
, since it represents a one-time call with ownership.) This model is analogous to thereplace_with_or_default
utility, which “takes ownership of a value at a mutable location, and replaces it with a new value based on the old one”. Like that utilityupdate_or_default
would move out theT
, applyf
, and then put a newT
back into theOption
.One could imagine an alternate design using a mutable reference, e.g.
This would insert the default
T
ifNone
, and then run a closure on&mut T
in place. Such a method (perhaps calledor_default_update
oror_default_modify
) would be a counterpart toHashMap::entry().and_modify()
. However, it only allows in-place mutation of the value and cannot change its type or replace it wholesale. By contrast, the proposedupdate_or_default(FnOnce(T) -> T)
covers cases where we want to replace the value with a computed new one. I note that both styles have merit: in-place modification vs. replacement. The primary goal here is to support the ownership-taking case, which is not directly addressable by any existing API.Comparison With Alternatives
get_or_insert_default
/get_or_insert_with
. These existing methods can fill in a missing value withT::default()
orf()
, but they return a&mut T
. For example,opt.get_or_insert_default()
gives a mutable reference to the containedT
. To perform a value-consuming update, the user would still need to do extra work, e.g. copying or moving out ofT
. In effect, one might write:which is clumsy. Unlike
update_or_default
, these methods do not allow the closure to consume the inner value; they only allow in-place mutation via a borrow. Thus they cannot express “take old T, produce new T” in one step.Using
Option::take()
. A common pattern is to usetake()
(which sets the option toNone
) and then rebuild theOption
. For example:or equivalently
opt = Some(opt.take().map(f).unwrap_or_default())
. While this works, it is verbose and repeats the insertion logic. It also momentarily leavesopt
asNone
, which may matter if panic occurs. Moreover, it may allocate or cloneT::default()
explicitly. In practice, this idiom is error-prone and hard to read. (The standard docs describeOption::take()
as “takes the value out of the option, leaving aNone
”, but they do not show the combined usage needed to update.)Option::replace
.opt.replace(new)
will swap in a new value and return the oldOption
. This is useful when you already have the new value in hand, but it doesn’t directly help when you only have a function to compute the new value. You would still have to dolet old = opt.take(); opt.replace(f(old.unwrap_or_default()));
. In other words, it doesn’t reduce any work for the user compared totake()
. It also requires constructing the newT
before callingreplace
.External crates (
replace_with
, etc.). Crates likereplace_with
offerreplace_with_or_default(&mut T, f)
which applies a closure or default on panic. These show the general pattern can be done: e.g.replace_with_or_default(dest, f)
“temporarily takes ownership of a value…and replace it with a new value”, usingT::default()
on panic. While instructive, relying on a crate is suboptimal when a simple method could be instd
. Moreover, theOption
-specific case is simpler to implement and use than the genericreplace_with
approach.In summary, each existing alternative either lacks convenience (extra boilerplate) or introduces complexity (borrowing vs owning). None directly address the “update-or-default” idiom in a single call. The proposed
update_or_default
fills this niche succinctly and safely.Safety Considerations
Because
update_or_default
would temporarily move out the contained value (if any), we must consider what happens if the closuref
panics. In a naive implementation (let old = opt.take(); let new = f(old); *opt = Some(new);
), a panic would leaveopt
in a None state (and the old value dropped). This is similar toreplace_with
, which notes: on panic it callsT::default()
to avoid leaving the original uninitialized. I recommend thatupdate_or_default
follow a similar philosophy to avoid “poisoning” theOption
: one could catch unwinds internally or otherwise guarantee that on panic theOption
ends up containingT::default()
instead of being empty. This ensures theOption
remains valid (though possibly holding a default-initializedT
). It places the same requirement as other panic-safe APIs:T::default()
must not panic, or else the process aborts (double-panic). Overall, the panic-safety ofupdate_or_default
should be at least as strong asreplace_with_or_default
. In practice, panics inf
are rare, and users still get a consistent default-initializedOption
rather than losing the value entirely.Naming Considerations
We have several candidate names, each with trade-offs:
update_or_default
(proposed). This name emphasizes that the method updates the existing value or uses a default ifNone
. It parallelsget_or_insert_default
. One might think it should perhaps betransform_or_default
orreplace_or_default
, but “update” is succinct and clearly implies in-place change semantics.or_default_transform
. This alternative highlights that we take the “or default” branch then transform. It is a bit more cumbersome to say, and deviates from usualOption
naming patterns (there is no otheror_default_*
family).replace_or_default
. This suggests replacing the old value or defaulting, but could be confused withOption::replace
. It also doesn’t clearly convey that a function/closure is involved.or_default_update
(for theFnOnce(&mut T)
variant). If an alternative method takingFnOnce(&mut T)
were added, a name likeor_default_update
oror_default_modify
could be used (mirroringentry().and_modify
). This would read like “or default, then update”.In my view,
update_or_default
is the most intuitive for theFnOnce(T) -> T
variant, clearly signifying “update the value, or default it”. The shorter name keeps the API surface tidy. I also note that usingFnOnce(T)
in the signature already distinguishes it; if we later add anFnOnce(&mut T)
variant, a different name (or_default_update
) would avoid confusion. The exact name can be debated, but it should capture the dual behavior (update or default) and the fact that a transformation is applied.Summary and Benefits
Adding
Option::update_or_default
would significantly improve ergonomics for a common pattern. It turns a multi-line idiom into a single, intention-revealing call. This makes code clearer and less error-prone: instead of jugglingtake()
,map_or_else
, orget_or_insert
, the programmer simply supplies a closure. The API parallels the convenience ofHashMap
’s entry API (e.g..and_modify(...).or_insert(...)
) but for the ubiquitousOption
type. In effect,update_or_default
givesOption
its own built-in “update-or-insert-default” functionality, akin to howHashMap::entry(key).or_default().and_modify()
does for maps.Overall, this proposal adds expressive power without breaking existing code. It leverages
Default::default()
as the fallback and embraces ownership semantics (FnOnce(T) -> T
) to cover mutable and non-Copy
types. The result is more concise and readable code for many scenarios: configuration management, state transitions, incremental data construction, and more. I believe the ergonomic benefits justify this addition to the standard library’s API.References: Rust’s official
Option
documentation and related examples;replace_with_or_default
crate docs;HashMap::Entry
docs for comparison.Links and Related Work
Option::unwrap_or_default
: ExtractsT
with a default, no in-place transformation.Option::get_or_insert_with
: Initializes but requires separate mutation.HashMap::entry().or_default().and_modify(f)
: Similar in-place update pattern.Map.prototype.set(key, map.get(key) ?? defaultValue)
for updating/initializing.std
.What Happens Now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team [feature lifecycle]. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible Responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn’t require any concrete solution or alternatives to have been proposed):
Second, if there’s a concrete solution:
The text was updated successfully, but these errors were encountered: