Description
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 was None
. Today, users resort to piecing together multiple methods. For example, Option::get_or_insert_default()
(and get_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 call let old = opt.take()
(which leaves None
) and then set opt = Some(f(old.unwrap_or_default()))
, but this is verbose and error-prone. Explicit match
or if let
arms also require duplicating the Some(…)
construction. In short, there is no single Option
method that cleanly expresses “if Some(x)
apply f(x)
, else set Some(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:// Verbose: take ownership, apply merge, and re-wrap config = Some(config.take().map_or_else(Default::default, |mut cfg| { cfg.merge(new_settings); cfg }));
The
map_or_else(Default::default, |cfg| …)
pattern is hard to read. Withupdate_or_default
, one could write simply:config.update_or_default(|mut cfg| { cfg.merge(new_settings); cfg });
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:state = Some(match state.take() { Some(s) => s.next(), // transform existing state None => State::default() // start new state });
This match is cumbersome. With
update_or_default
, it becomes:state.update_or_default(|s| s.next());
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:if let Some(vec) = &mut maybe_vec { vec.push(item); } else { maybe_vec = Some(vec![item]); }
Or equivalently:
maybe_vec = Some(maybe_vec.take().map_or_else(Vec::new, |mut v| { v.push(item); v }));
Both forms are lengthy. With the proposed API, it would be:
maybe_vec.update_or_default(|mut v| { v.push(item); v });
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:
impl<T: Default> Option<T> {
pub fn update_or_default<F>(&mut self, f: F)
where F: FnOnce(T) -> T;
}
Semantically, this does: if self
is Some(x)
, replace it with Some(f(x))
; if self
is None
, insert Some(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 closure f
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 old T
without requiring T: Clone
or extra copies. (All closures implement FnOnce
, since it represents a one-time call with ownership.) This model is analogous to the replace_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 utility update_or_default
would move out the T
, apply f
, and then put a new T
back into the Option
.
One could imagine an alternate design using a mutable reference, e.g.
fn or_default_update<F>(&mut self, f: F)
where F: FnOnce(&mut T);
This would insert the default T
if None
, and then run a closure on &mut T
in place. Such a method (perhaps called or_default_update
or or_default_modify
) would be a counterpart to HashMap::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 proposed update_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:// Workaround: manually take and reassign let mut tmp = std::mem::take(opt.get_or_insert_default()); *opt.get_or_insert_default() = f(tmp);
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:opt = Some(opt.take().map_or_else(Default::default, |v| f(v)));
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 closure f
panics. In a naive implementation (let old = opt.take(); let new = f(old); *opt = Some(new);
), a panic would leave opt
in a None state (and the old value dropped). This is similar to replace_with
, which notes: on panic it calls T::default()
to avoid leaving the original uninitialized. I recommend that update_or_default
follow a similar philosophy to avoid “poisoning” the Option
: one could catch unwinds internally or otherwise guarantee that on panic the Option
ends up containing T::default()
instead of being empty. This ensures the Option
remains valid (though possibly holding a default-initialized T
). 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 of update_or_default
should be at least as strong as replace_with_or_default
. In practice, panics in f
are rare, and users still get a consistent default-initialized Option
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 the FnOnce(T) -> T
variant, clearly signifying “update the value, or default it”. The shorter name keeps the API surface tidy. I also note that using FnOnce(T)
in the signature already distinguishes it; if we later add an FnOnce(&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 juggling take()
, map_or_else
, or get_or_insert
, the programmer simply supplies a closure. The API parallels the convenience of HashMap
’s entry API (e.g. .and_modify(...).or_insert(...)
) but for the ubiquitous Option
type. In effect, update_or_default
gives Option
its own built-in “update-or-insert-default” functionality, akin to how HashMap::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
- Rust:
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.
- Other Languages:
- JavaScript:
Map.prototype.set(key, map.get(key) ?? defaultValue)
for updating/initializing.
- JavaScript:
- Discussions:
- No prior Internals thread, but this ACP is inspired by ergonomic patterns in
std
. - RFC discussion: RFC.
- No prior Internals thread, but this ACP is inspired by ergonomic patterns in
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):
- We think this problem seems worth solving, and the standard library might be the right place to solve it.
- We think that this probably doesn’t belong in the standard library.
Second, if there’s a concrete solution:
- We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
- We’re not sure this is the right solution, and the alternatives or other materials don’t give us enough information to be sure about that. Here are some questions we have that aren’t answered, or rough ideas about alternatives we’d want to see discussed.