Skip to content

ACP: Add Entry-like API for Option #575

Closed as not planned
Closed as not planned

Description

@Kivooeo

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 steps
  • take() + map_or_else() + reassignment
  • Manual match expressions with repeated Some() 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 initialization
  • update() handles in-place mutations via &mut T
  • replace_with() handles value-consuming transformations via T -> 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 consume self or 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:

  1. Familiar patterns - developers know Entry APIs
  2. Composability - natural method chaining
  3. Extensibility - easy to add new operations later
  4. Clear separation - each method has a single responsibility
  5. 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

  1. pitaj commented on Apr 19, 2025

    @pitaj
    1. You seem to be unaware of get_or_insert_default:
    let mut opt: Option<String> = None;
    opt.get_or_insert_default().push_str("Hello");

    Calls T::default() only if None. For types like String, this may allocate (e.g., String::new()). Users with expensive defaults can use get_or_insert_with.

    nit: String::new() does not allocate.

    pub fn update_or_default<F>(&mut self, f: F)
    where
        F: FnOnce(T) -> T,
        T: Default,
    let mut opt: Option<String> = None;
    opt.update_or_default(|mut s| { s.push_str("Hello"); s });
    assert_eq!(opt, Some("Hello".to_string()));
    1. The name is misleading, as it makes it sound like the function either inserts default if None or updates the value of Some.

    2. 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 &mut instead:

    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"));
    1. The utility of even that is dubious compared to the get_or_insert_default version above.

    You should provide some better examples that actually make use of your version passing it by value.

    1. Finally, don't you want a T or &mut T out of something like this? You just went through all this trouble to make sure it's Some, why would you want to just unwrap it again later?

    And if you're going to get a T or &mut T, why not just do the mutation separately?

  2. Kivooeo commented on Apr 19, 2025

    @Kivooeo
    MemberAuthor

    @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’s Some(T), or create a T::default() if it’s None and then apply the function, storing the result back in the Option. This combines initialization and transformation in one ergonomic step, especially for cases where the function consumes T to produce a new T

    You're right that get_or_insert_default simplifies initializing an Option with T::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 new T from the old one). The proposed update_or_default aims 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_default needs brainstorming, because it’s honestly more like or_default_transform or replace_or_default (I’ll use update_or_default in examples below for consistency). Your suggestion of or_default_update feels 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_update taking a closure FnOnce(&mut T) is compelling for in-place mutations, and I agree it’s more ergonomic in many cases. However, there are scenarios where a FnOnce(T) -> T closure 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 why FnOnce(T) -> T can be valuable.

    #[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 Config struct may be incrementally built by merging user-provided options. The Config is stored in an Option<Config>, and we want to merge a new set of options into the existing config or start with a default if none exists

    The transition method consumes the GameState, making FnOnce(T) -> T necessary. The proposed API makes this concise and safe.

    #[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 T or &mut T after the update. In most cases, the Option is mutated in place, and users don’t immediately unwrap it—they continue working with the Option (e.g., in a loop or later logic). The examples above show scenarios where the Option remains Some after the update, and subsequent operations can chain or access it.

    Next Steps

    Based on your feedback, I propose:

    1. Renaming to update_or_default for clarity.
    2. Keeping FnOnce(T) -> T as the primary signature to support value-consuming transformations, with examples like those above.
    3. Exploring a &mut T-returning variant or a separate method for in-place mutations (e.g., or_default_mutate with FnOnce(&mut T)).
    4. Correcting the String::new() allocation note and clarifying performance implications.
  3. magistau commented on Apr 19, 2025

    @magistau

    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) with FnOnce(T) -> T. In particular, the value gets "poisoned" if the closure panics. With that in mind, your suggestion seems to be equivalent to replace_with(opt.get_or_insert_default(), f).

  4. quaternic commented on Apr 27, 2025

    @quaternic

    There’s no standard method to combine default insertion and in-place transformation in a single, ergonomic operation.

    Since it returns &mut T, get_or_insert_default is made for that.

    For by-value updates, another alternative is

    opt.get_or_insert_default();
    opt = opt.take().map(some_update_func);
  5. Kivooeo commented on Apr 27, 2025

    @Kivooeo
    MemberAuthor

    @quaternic

    Thank you for your feedback!

    You're right that get_or_insert_default nicely 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_default is 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_default followed by clone (to avoid mutating through a shared reference) or take().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.

  6. the8472 commented on Jul 8, 2025

    @the8472
    Member

    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 T out at the end.

  7. Kivooeo commented on Jul 8, 2025

    @Kivooeo
    MemberAuthor

    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 T approach, what if we added both methods:

    • One with &mut T for in-place modifications,
    • And another that takes FnOnce(T) -> T for 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: Default because we do not provide a default value as a parameter.

    Looking forward to further discussion!

  8. the8472 commented on Jul 8, 2025

    @the8472
    Member

    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.

  9. Kivooeo commented on Jul 9, 2025

    @Kivooeo
    MemberAuthor

    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

  10. Kivooeo commented on Jul 9, 2025

    @Kivooeo
    MemberAuthor

    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

    let mut config: Option<Config> = None;
    
    // For in-place mutations (equivalent to get_or_insert_default + mutation)
    config.or_default().update(|c| c.log_level = 3);
    
    // For by-value transformations (your main use case)
    config.or_default().replace_with(|c| c.merge(partial));
    
    // State machine example
    state.or_default().replace_with(|s| s.transition(event));
    
    // Chaining operations
    let final_config = config.or_default()
        .update(|c| c.log_level = 2)
        .replace_with(|c| c.merge(other));
    
  11. pitaj commented on Jul 9, 2025

    @pitaj

    For reference, I've implemented an Option entry API for use in fixed-map:

    https://github.com/udoprog/fixed-map/blob/main/src/option_bucket.rs

  12. changed the title [-]ACP: `Option::update_or_default`[/-] [+]ACP: Add Entry-like API for Option[/+] on Jul 9, 2025
  13. Kivooeo commented on Jul 9, 2025

    @Kivooeo
    MemberAuthor

    I completly rewrote this proposal into an Entry API, and frankly, it's night and day better. We get HashMap::Entry patterns 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?

  14. RustyYato commented on Jul 9, 2025

    @RustyYato

    It seems like you can go farther and just take the entire entry API, instead of just the OccupiedEntry API. Something like this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b7ad6676d423e4c405e80f6e7530da62

    use OptionEntry::{Vacant, Occupied};
    
    pub enum OptionEntry<'a, T> {
        Vacant(VacantOptionEntry<'a, T>),
        Occupied(OccupiedOptionEntry<'a, T>),
    }
    pub struct VacantOptionEntry<'a, T> {
        slot: &'a mut Option<T>
    }
    
    pub struct OccupiedOptionEntry<'a, T> {
        slot: &'a mut Option<T>
    }
    
    impl Option<T> {
        fn entry(&mut self) -> OptionEntry<'_, T>;
    }
    
    impl<'a, T> OptionEntry<'a, T> {
        pub fn insert(self, value: T) -> &'a mut T;
        pub fn insert_entry(self, value: T) -> OccupiedOptionEntry<'a, T>;
        pub fn or_insert(self, value: T) -> OccupiedOptionEntry<'a, T>;
        pub fn or_insert_with(self, value: impl FnOnce() -> T) -> OccupiedOptionEntry<'a, T>;
        pub fn and_modify(self, f: impl FnOnce(&mut T)) -> OptionEntry<'a, T>;
        pub fn or_default(self) -> OccupiedOptionEntry<'a, T> where T: Default;
    }
    
    impl<'a, T> VacantOptionEntry<'a, T> {
        pub fn insert(self, value: T) -> OccupiedOptionEntry<'a, T>;
    }
    
    impl<'a, T> OccupiedOptionEntry<'a, T> {
        pub fn into_mut(self) -> &'a mut T;
        pub fn get_mut(&mut self) -> &mut T;
        pub fn get(&self) -> &T;
        pub fn insert(&mut self, value: T) -> T;
        pub fn remove(self) -> T;
        // this one doesn't exist in Hashmap entry, but maybe it could be added?
        // otherwise this could be dropped very easily
        pub fn remove_entry(self) -> (VacantOptionEntry<'a, T>, T);
    }
  15. Kivooeo commented on Jul 9, 2025

    @Kivooeo
    MemberAuthor

    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

  16. Amanieu commented on Jul 15, 2025

    @Amanieu
    Member

    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:

    // Before
    config = Some(config.take().map_or_else(Default::default, |cfg| cfg.merge(partial)));
    
    // Before, improved
    config = Some(config.take().unwrap_or_default().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();
    
    // Before, improved
    let new_state = state.take().unwrap_or_default();
    let mut new_state = new_state.transition(event);
    new_state.validate();
    state.insert(new_state);
    
    // 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();
    
    // Before, improved
    let inner = items.get_or_insert_default();
    inner.push(new_item);
    inner.sort();
    
    // After
    items.or_default()
        .update(|vec| vec.push(new_item))
        .update(|vec| vec.sort());
  17. Kivooeo commented on Jul 15, 2025

    @Kivooeo
    MemberAuthor

    @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):

    // Current
    let mut kv = self.txt.take().unwrap_or_default();
    kv.insert(key, value);
    self.txt = Some(kv);
    
    // With Entry API
    self.txt.or_default().update(|kv| kv.insert(key, value));

    Flag/configuration building (compiletest-rs):

    // Current
    let mut flags = self.target_rustcflags.take().unwrap_or_default();
    flags += lib_paths_flags(&lib_paths).as_str();
    self.target_rustcflags = Some(flags);
    
    // With Entry API
    self.target_rustcflags
        .or_default()
        .update(|flags| *flags += lib_paths_flags(&lib_paths).as_str());

    State management (LaikaStudios/shotgrid-rs):

    // Current
    let mut pagination = self.pagination.take().unwrap_or_default();
    if pagination.number.is_none() && value.is_none() {
        // validation logic
    }
    self.pagination = Some(pagination);
    
    // With Entry API
    self.pagination.or_default().update(|p| {
        if p.number.is_none() && value.is_none() {
            // validation logic
        }
    });

    The key insight is that developers already understand and trust this pattern from HashMap:

    // HashMap - familiar and composable
    map.entry(key)
        .or_insert_with(|| expensive_computation())
        .and_modify(|counter| *counter += 1)
        .or_insert(1);
    
    // Current Option approach - verbose and error-prone
    let value = opt.take().unwrap_or_else(|| expensive_computation());
    let new_value = transform_value(value);
    opt = Some(new_value);
    if let Some(ref mut v) = opt {
        *v += 1;
    }
    
    // Option with Entry API - same mental model as HashMap
    opt.or_insert_with(|| expensive_computation())
        .replace_with(|v| transform_value(v))
        .update(|v| *v += 1);

    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:

    // current, sure, we can nest it, but with a cost of readability, is this worth to?
    let mut temp = data.take().unwrap_or_default();
    temp = transform_step1(temp)?;
    if condition {
        temp = transform_step2(temp);
    }
    let result = finalize(temp);
    data = Some(result);
    
    // new API 
    data.entry()
        .replace_with(transform_step1)
        .update_if(condition, |d| transform_step2(d))
        .replace_with(finalize);
  18. wmstack commented on Aug 6, 2025

    @wmstack

    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 Entry API for Option.

    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.

    // accepted solution
    match &mut data {
         Some(val) => {…},
         opt @ None => *opt = Some(value),
    }
    
    // much more obvious
    data.entry().and_modify(|val| …).or_insert(value)
  19. Kivooeo commented on Aug 6, 2025

    @Kivooeo
    MemberAuthor

    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!

  20. Amanieu commented on Aug 11, 2025

    @Amanieu
    Member

    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:

    For all of the examples you listed, you can use get_or_insert_default to get just as good or better ergonomics as what you are proposing.

    Builder patterns (AstroHQ/astro-dnssd):

    // Current
    let mut kv = self.txt.take().unwrap_or_default();
    kv.insert(key, value);
    self.txt = Some(kv);
    
    // With get_or_insert_default
    self.txt.get_or_insert_default().insert(key, value);
    
    // With Entry API
    self.txt.or_default().update(|kv| kv.insert(key, value));

    Flag/configuration building (compiletest-rs):

    // Current
    let mut flags = self.target_rustcflags.take().unwrap_or_default();
    flags += lib_paths_flags(&lib_paths).as_str();
    self.target_rustcflags = Some(flags);
    
    // With get_or_insert_default
    *self.target_rustcflags.get_or_insert_default() += lib_paths_flags(&lib_paths).as_str();
    
    // With Entry API
    self.target_rustcflags
        .or_default()
        .update(|flags| *flags += lib_paths_flags(&lib_paths).as_str());

    State management (LaikaStudios/shotgrid-rs):

    // Current
    let mut pagination = self.pagination.take().unwrap_or_default();
    if pagination.number.is_none() && value.is_none() {
        // validation logic
    }
    self.pagination = Some(pagination);
    
    // With get_or_insert_default
    let pagination = self.pagination.get_or_insert_default();
    if pagination.number.is_none() && value.is_none() {
        // validation logic
    }
    
    // With Entry API
    self.pagination.or_default().update(|p| {
        if p.number.is_none() && value.is_none() {
            // validation logic
        }
    });

    Fundamentally, the get_or_insert*/insert APIs already provide all the functionality you need in practice for accessing the inside of an Option.

    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 tap crate. Making the Option API significantly more complicated by adding an entire Entry API seems overkill for this.

  21. Kivooeo commented on Aug 11, 2025

    @Kivooeo
    MemberAuthor

    @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 Entry API.

    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 HashMap patterns and potentially guiding users toward better practices rather than the current take().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?

  22. kennytm commented on Aug 12, 2025

    @kennytm
    Member

    if the only issue is the .take().unwrap_or_default() pattern maybe this could be encoded into a clippy lint.

  23. Amanieu commented on Aug 12, 2025

    @Amanieu
    Member

    I suspect that one of the main reasons why get_or_insert_default isn't as widely used is that it was only stabilized relatively recently in 1.83 (Nov 2024).

  24. Kivooeo commented on Aug 12, 2025

    @Kivooeo
    MemberAuthor

    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 with get_or_insert_default

    But I'am freezing it for now to see if get_or_insert_default will become commonly used with better documentation and more references in other methods

    Also just FYI, originally Entry API for Option was proposed by team, not by me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions