Repository navigation
ACP: Add Entry-like API for Option #575
Description
Revised ACP: Option Entry API
Problem Statement
The current Option<T> API lacks a fluent, composable interface for the common "initialize-or-update" pattern. While get_or_insert_default() handles basic cases, real-world code often needs to chain multiple operations: initialize with default, perform in-place mutations, apply value-consuming transformations, or combinations thereof.
Today's solutions are fragmented and verbose:
get_or_insert_default()+ separate mutation stepstake()+map_or_else()+ reassignment- Manual
matchexpressions with repeatedSome()wrapping
This fragmentation leads to boilerplate, potential logic errors, and reduced readability.
Proposed Solution
Introduce an Entry-like API for Option<T>, inspired by HashMap::Entry but tailored for the Option use case:
impl<T> Option<T> {
pub fn or_default(&mut self) -> OptionEntry<'_, T>
where T: Default;
pub fn or_insert_with<F>(&mut self, f: F) -> OptionEntry<'_, T>
where F: FnOnce() -> T;
}
pub struct OptionEntry<'a, T> {
option: &'a mut Option<T>,
}
impl<T> OptionEntry<'_, T> {
pub fn update<F>(self, f: F) -> Self
where F: FnOnce(&mut T);
pub fn replace_with<F>(self, f: F) -> Self
where F: FnOnce(T) -> T;
// this may be added later if needed
pub fn get_mut(&mut self) -> &mut T;
pub fn into_inner(self) -> &mut T;
}Key Design Decisions
Entry-based approach: Rather than adding individual methods to Option, we provide an entry point that enables method chaining. This mirrors the successful HashMap::Entry pattern that developers already understand.
Separation of concerns:
or_default()handles initializationupdate()handles in-place mutations via&mut Treplace_with()handles value-consuming transformations viaT -> T
Composability: Methods return Self, allowing natural chaining of operations.
Motivating Examples
Configuration merging:
// Before
config = Some(config.take().map_or_else(Default::default, |cfg| cfg.merge(partial)));
// After
config.or_default().replace_with(|cfg| cfg.merge(partial));State machine with validation:
// Before
state = Some(state.take().unwrap_or_default().transition(event));
let s = state.as_mut().unwrap();
s.validate();
// After
state.or_default()
.replace_with(|s| s.transition(event))
.update(|s| s.validate());Incremental building:
// Before
items.get_or_insert_default().push(new_item);
items.get_or_insert_default().sort();
// After
items.or_default()
.update(|vec| vec.push(new_item))
.update(|vec| vec.sort());Addressing Previous Concerns
Panic safety: The entry maintains the invariant that the Option always contains a value after or_default(). If a closure in update() or replace_with() panics, the Option retains its current valid state. For replace_with(), we use standard unwind-safe techniques to ensure the Option contains either the new value or the original value—never None.
Performance: The entry borrows the Option mutably, avoiding temporary None states entirely. No additional allocations or moves beyond what the user's closures require.
When to use &mut T vs T -> T:
- Use
update()for in-place mutations (incrementing counters, pushing to vectors, setting fields) - Use
replace_with()when methods consumeselfor when constructing fundamentally new values
Ergonomics over existing alternatives:
- More discoverable than
get_or_insert_default()+ manual steps - Safer than
take()+unwrap_or_default()patterns - More composable than individual specialized methods
Comparison with HashMap::Entry
Developers already understand this pattern:
// HashMap
map.entry(key)
.and_modify(|v| *v += 1)
.or_insert(0);
// Option (proposed)
opt.or_default()
.update(|v| *v += 1);The mental model transfers directly while addressing Option-specific needs.
Implementation Considerations
The entry struct is zero-cost—it's essentially a typed wrapper around &mut Option<T>. The or_default() method ensures the Option contains a value, and subsequent operations work on that guarantee.
For panic safety in replace_with(), we can use the same approach as std::mem::replace() with unwind guards, or document that panic during replacement leaves the original value intact.
Why This Approach
Rather than adding multiple methods to Option directly, the entry approach provides:
- Familiar patterns - developers know Entry APIs
- Composability - natural method chaining
- Extensibility - easy to add new operations later
- Clear separation - each method has a single responsibility
- Type safety - the entry guarantees the Option contains a value
This design addresses the core use case (update-or-default) while providing a foundation for future Option manipulation needs.
Alternative Considered
The original update_or_default(&mut self, f: FnOnce(T) -> T) proposal was simpler but less flexible. It couldn't handle the common case of in-place mutations, couldn't chain operations, and would require additional similar methods for other patterns.
The Entry API generalizes better and provides a more complete solution to Option manipulation ergonomics.
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.
Activity
pitaj commentedon Apr 19, 2025
get_or_insert_default:nit:
String::new()does not allocate.The name is misleading, as it makes it sound like the function either inserts default if None or updates the value of Some.
This doesn't look very ergonomic for the cases you describe. It looks like what you really want is one where the function gets a
&mutinstead:get_or_insert_defaultversion above.You should provide some better examples that actually make use of your version passing it by value.
Tor&mut Tout of something like this? You just went through all this trouble to make sure it'sSome, why would you want to just unwrap it again later?And if you're going to get a
Tor&mut T, why not just do the mutation separately?Kivooeo commentedon Apr 19, 2025
@pitaj
Before I want to clarify intention of this method, because it could be not clearly describe in the ACP:
The intention is to apply a function to the T inside an
Option<T>if it’sSome(T), or create aT::default()if it’sNoneand then apply the function, storing the result back in theOption. This combines initialization and transformation in one ergonomic step, especially for cases where the function consumesTto produce a newTYou're right that
get_or_insert_defaultsimplifies initializing anOptionwithT::default()when None. However, it returns a&mut T, which works well for in-place mutations but doesn't cover cases where the transformation involves consuming and replacing the value (e.g., returning a newTfrom the old one). The proposedupdate_or_defaultaims to handle both in-place mutations and value-consuming transformations in a single ergonomic operation, reducing the need for separate initialization and mutation steps.I very agree that
update_or_defaultneeds brainstorming, because it’s honestly more likeor_default_transformorreplace_or_default(I’ll useupdate_or_defaultin examples below for consistency). Your suggestion ofor_default_updatefeels closer, as it emphasizes inserting a default and transforming the value, but I think we can refine it further to capture the “consume-and-replace” intent. I’d love your thoughts on these or other ideas.Your suggestion for
or_default_updatetaking a closureFnOnce(&mut T)is compelling for in-place mutations, and I agree it’s more ergonomic in many cases. However, there are scenarios where aFnOnce(T) -> Tclosure is more natural, particularly when the transformation consumes the value or constructs a new one. Let me provide some concrete, real-world-inspired examples to illustrate both cases and whyFnOnce(T) -> Tcan be valuable.In a CLI application, a
Configstruct may be incrementally built by merging user-provided options. TheConfigis stored in anOption<Config>, and we want to merge a new set of options into the existing config or start with a default if none existsThe transition method consumes the
GameState, makingFnOnce(T) -> Tnecessary. The proposed API makes this concise and safe.You raised a great point about whether users want a
Tor&mut Tafter the update. In most cases, theOptionis mutated in place, and users don’t immediately unwrap it—they continue working with theOption(e.g., in a loop or later logic). The examples above show scenarios where theOptionremainsSomeafter the update, and subsequent operations can chain or access it.Next Steps
Based on your feedback, I propose:
update_or_defaultfor clarity.FnOnce(T) -> Tas the primary signature to support value-consuming transformations, with examples like those above.&mut T-returning variant or a separate method for in-place mutations (e.g.,or_default_mutatewithFnOnce(&mut T)).String::new()allocation note and clarifying performance implications.magistau commentedon Apr 19, 2025
This is related to rust-lang/rfcs#1736, you might want to see its discussion for why it is not easy to replace
FnOnce(&mut T)withFnOnce(T) -> T. In particular, the value gets "poisoned" if the closure panics. With that in mind, your suggestion seems to be equivalent toreplace_with(opt.get_or_insert_default(), f).quaternic commentedon Apr 27, 2025
Since it returns
&mut T,get_or_insert_defaultis made for that.For by-value updates, another alternative is
Kivooeo commentedon Apr 27, 2025
@quaternic
Thank you for your feedback!
You're right that
get_or_insert_defaultnicely covers the case of in-place mutation via&mut T, and for many scenarios, it is indeed sufficient and very ergonomic.However, the main motivation behind
update_or_defaultis to better support by-value transformations -- that is, when you want to consume the existing value and produce a new one.In such cases,
get_or_insert_defaultfollowed by clone (to avoid mutating through a shared reference) ortake().map(f)(after inserting default) can get verbose, error-prone, or inefficient (especially if clone is expensive).The goal is to make this pattern simpler and safer by offering a single, ergonomic method.
the8472 commentedon Jul 8, 2025
We discussed this ACP in today's libs-api meeting. There were some concerns about the value being None in case of a panic, but re-reading your proposal I see you're suggesting to leave it as Default instead, I guess we'll have to revisit that.
Other than that we discussed that having something closer to the hashmap Entry API might be more modular, allowing composition of the update and default methods and possibly others, chaining them and finally getting a
&mut Tout at the end.Kivooeo commentedon Jul 8, 2025
Thanks for the feedback! Honestly, the proposal definitely needs polishing after all this time, and I'm very open to hearing other ideas.
Regarding the
&mut Tapproach, what if we added both methods:&mut Tfor in-place modifications,FnOnce(T) -> Tfor full replacement.I believe both use cases are popular in their own ways. I described the second one in more detail in the proposal and discussion.
My main concern with the second variant was panic safety, but since it’s acceptable to insert
Default::default()on panic, I’m happy with that.Also, I want to point out something that might have been missed: this API would require
T: Defaultbecause we do not provide a default value as a parameter.Looking forward to further discussion!
the8472 commentedon Jul 8, 2025
Another question that came up was if you had examples where it would be necessary to take ownership instead of performing the update via &mut.
Kivooeo commentedon Jul 9, 2025
Well, in this comment I shared examples where methods consume self and return a new Self. That’s why update_or_default helps a lot. I’m not quite sure how the same would work with &mut T
Kivooeo commentedon Jul 9, 2025
Hm, I did take a look to an Entry like API variant, and I really like it, here possible very first version of how it could look, only thing that I'm not sure about is naming
pitaj commentedon Jul 9, 2025
For reference, I've implemented an
Optionentry API for use in fixed-map:https://github.com/udoprog/fixed-map/blob/main/src/option_bucket.rs
[-]ACP: `Option::update_or_default`[/-][+]ACP: Add Entry-like API for Option[/+]Kivooeo commentedon Jul 9, 2025
I completly rewrote this proposal into an Entry API, and frankly, it's night and day better. We get
HashMap::Entrypatterns that developers already understand, natural method chaining, clear separation of concerns, and zero additional complexity. This addresses every concern raised while being significantly more powerful than the original.I'd love to hear what others think about this new direction. Are there any concerns with the Entry approach that I haven't considered?
RustyYato commentedon Jul 9, 2025
It seems like you can go farther and just take the entire entry API, instead of just the
OccupiedEntryAPI. Something like this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b7ad6676d423e4c405e80f6e7530da62Kivooeo commentedon Jul 9, 2025
Fair point - the full Entry API would be more consistent with HashMap. I'd lean toward starting with the simplified approach and extending it later, but honestly the team will decide what fits best with std's design principles. Let's see what they think
Amanieu commentedon Jul 15, 2025
I went through the examples and improved them using the methods that we already have. I don't think the new API provides a sufficient improvement to justify the API complexity.
Motivating Examples
Configuration merging:
State machine with validation:
Incremental building:
Kivooeo commentedon Jul 15, 2025
@Amanieu Thanks for the improved examples! While they're definitely more readable, I think the core ergonomics issue remains. The pattern still requires intermediate variables and manual
Some()rewrapping, which is error-prone in builder patterns.I did a GH search for
take().unwrap_or_default()and found thousands (and it's only this pattern, there is lot ways to write it differently) of real examples that would benefit from this API. Here are some from production code that illustrate the pattern:Builder patterns (AstroHQ/astro-dnssd):
Flag/configuration building (compiletest-rs):
State management (LaikaStudios/shotgrid-rs):
The key insight is that developers already understand and trust this pattern from HashMap:
Beyond just syntax, this eliminates the manual state management that's easy to get wrong. The Entry API keeps the Option valid throughout and leverages a pattern developers already know well from HashMap work.
I think the production examples show that this isn't just theoretical - it's a common enough pattern that having idiomatic, safe support for it would be valuable.
More hard example is becoming more valuable this API becomes:
wmstack commentedon Aug 6, 2025
I like the Entry-API for Option, it seems quite fluent and obvious to work with for me. For example, consider this recent discussion on how to do the update or insert pattern, which seems like it could've been solved fairly simply with an
EntryAPI forOption.https://users.rust-lang.org/t/update-existing-option-if-some-or-set-to-some/110242/9
Instead, there was a back and forth discussion that took quite some time to get resolved.
Kivooeo commentedon Aug 6, 2025
Hi @the8472,
Would it be possible worth bringing this redesigned Entry API approach up in the next meeting? I believe my previous response addresses the core value proposition with real production examples.
The current Entry API proposal is quite flexible - we can adapt it to address any team concerns while preserving the ergonomic benefits. Happy to iterate on the design!
Amanieu commentedon Aug 11, 2025
For all of the examples you listed, you can use
get_or_insert_defaultto get just as good or better ergonomics as what you are proposing.Builder patterns (AstroHQ/astro-dnssd):
Flag/configuration building (compiletest-rs):
State management (LaikaStudios/shotgrid-rs):
Fundamentally, the
get_or_insert*/insertAPIs already provide all the functionality you need in practice for accessing the inside of anOption.What you actually want is a way to make chaining methods easier, which is probably better served by a generic mechanism such as the one provided by the
tapcrate. Making theOptionAPI significantly more complicated by adding an entireEntryAPI seems overkill for this.Kivooeo commentedon Aug 11, 2025
@Amanieu, I appreciate the feedback! Just to clarify - I'm proposing an "Entry-like" approach with just a few methods for start, not the entire
EntryAPI.I understand these examples can be rewritten with existing methods. But the interesting question is: why aren't thousands of developers naturally discovering these better patterns? When the same "suboptimal" pattern appears consistently across codebases, it might indicate a discoverability issue rather than a knowledge gap.
The Entry-like approach could serve as a "bridge" - familiar from
HashMappatterns and potentially guiding users toward better practices rather than the currenttake().unwrap_or_default()antipattern.What do you think about the discoverability angle? Is there value in API design that aligns with developer mental models, even when technically equivalent solutions exist?
kennytm commentedon Aug 12, 2025
if the only issue is the
.take().unwrap_or_default()pattern maybe this could be encoded into a clippy lint.Amanieu commentedon Aug 12, 2025
I suspect that one of the main reasons why
get_or_insert_defaultisn't as widely used is that it was only stabilized relatively recently in 1.83 (Nov 2024).Kivooeo commentedon Aug 12, 2025
That's the problem that's there is not one only problem pattern, there is a couple of ways to write the same thing bad, and, not always
.take().unwrap_or_default()could be replacen withget_or_insert_defaultBut I'am freezing it for now to see if
get_or_insert_defaultwill become commonly used with better documentation and more references in other methodsAlso just FYI, originally Entry API for Option was proposed by team, not by me