rust-lang / libs-team

The home of the library team
Apache License 2.0
123 stars 19 forks source link

Add `or_try_*` variants for HashMap and BTreeMap Entry APIs #336

Open peterjoel opened 7 months ago

peterjoel commented 7 months ago

Proposal

Problem statement

The HashMap and BTreeMap entry APIs make it possible to operate on an item that is either already in a map or on a newly inserted item if one wasn't yet present, while only needing a single lookup.

The limitation with the current API is that methods like or_insert_with do not allow for item construction that can fail.

Motivating examples or use cases

I found several cases when moving to work on an existing codebase, where code was commented to explain why this had to be manually implemented instead of using the Entry API. The examples there are a little complex though, and I'll show some simpler ones.

Without the proposed new methods, it is either verbose:

fn main() -> Result<(), std::num::ParseIntError> {
    let mut map: HashMap<&str, usize> = HashMap::new();
    let value = "42";

    match map.entry("poneyland") {
        Entry::Occupied(mut entry) => {
            *entry.get_mut() += 1;
        }
        Entry::Vacant(entry) => {
            entry.insert(value.parse()?);
        }
    };

    assert_eq!(map["poneyland"], 42);
    Ok(())
}

Or inefficient (multiple hashmap lookups):

fn main() -> Result<(), std::num::ParseIntError> {
    let mut map: HashMap<&str, usize> = HashMap::new();
    let value = "42";

    if let Some(value) = map.get_mut("poneyland") {
        *value+= 1;
    } else {
        map.insert("poneyland", value.parse()?);
    }

    assert_eq!(map["poneyland"], 42);
    Ok(())
}

With the proposed changes:

fn main() -> Result<(), std::num::ParseIntError> {
    let mut map: HashMap<&str, usize> = HashMap::new();
    let value = "42";

    map.entry("poneyland")
        .and_modify(|entry| *entry += 1)
        .or_try_insert_with(|| value.parse())?;

    assert_eq!(map["poneyland"], 42);
    Ok(())
}

Solution sketch

The method signatures look like this:

impl Entry<K, V> {

    pub fn or_try_insert_with<F: FnOnce() -> Result<V, E>, E>(
        self,
        default: F,
    ) -> Result<&'a mut V, E>;

    pub fn or_try_insert_with_key<F: FnOnce(&K) -> Result<V, E>, E>(
        self,
        default: F,
    ) -> Result<&'a mut V, E>;
}

I have complete implementations in this PR: https://github.com/rust-lang/rust/pull/120708/files

Alternatives

It is already possible to implement these methods by explicitly matching on the the Entry enum variants (see motivating examples), but this is far less ergonomic than the proposed methods. Similarly, or_insert_with could be implemented by users themselves, but it is included in the standard library because it is more streamlined than an explicit match expression.

A third party crate could also offer the same functionality using an extension trait. It's hard to imagine a crate being successful with only this feature though, though it could be appropriate in a "grab-bag" utility crate.

Finally, it's possible to use the existing Entry methods if you handle errors by panicking.

Links and related work

An implementation for HashMap's entry API:

https://github.com/rust-lang/rust/pull/120708

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: