Skip to content
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

Add {BTreeMap,HashMap}::try_insert #82764

Merged
merged 7 commits into from
Mar 5, 2021
Merged

Conversation

m-ou-se
Copy link
Member

@m-ou-se m-ou-se commented Mar 4, 2021

{BTreeMap,HashMap}::insert(key, new_val) returns Some(old_val) if the key was already in the map. It's often useful to assert no duplicate values are inserted.

We experimented with map.insert(key, val).unwrap_none() (#62633), but decided that that's not the kind of method we'd like to have on Options.

insert always succeeds because it replaces the old value if it exists. One could argue that insert() is never the right method for panicking on duplicates, since already handles that case by replacing the value, only allowing you to panic after that already happened.

This PR adds a try_insert method that instead returns a Result::Err when the key already exists. This error contains both the OccupiedEntry and the value that was supposed to be inserted. This means that unwrapping that result gives more context:

    map.insert(10, "world").unwrap_none();
    // thread 'main' panicked at 'called `Option::unwrap_none()` on a `Some` value: "hello"', src/main.rs:8:29
    map.try_insert(10, "world").unwrap();
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
    // OccupiedError { key: 10, old_value: "hello", new_value: "world" }', src/main.rs:6:33

It also allows handling the failure in any other way, as you have full access to the OccupiedEntry and the value.

try_insert returns a reference to the value in case of success, making it an alternative to .entry(key).or_insert(value).

r? @Amanieu

Fixes rust-lang/rfcs#3092

@m-ou-se m-ou-se added A-collections Area: `std::collection` T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Mar 4, 2021
@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Mar 4, 2021
library/alloc/src/collections/btree/map.rs Outdated Show resolved Hide resolved
Comment on lines +79 to +82
/// The entry in the map that was already occupied.
pub entry: OccupiedEntry<'a, K, V>,
/// The value which was not inserted, because the entry was already occupied.
pub value: V,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made these fields public instead of adding accessors. This is somewhat uncommon in std, but we do the same for std::ops::Range.

Accessor functions here would get somewhat messy, because taking ownership of one or both of the fields should be possible. The alternative is .entry(), .value(), .into_entry(), .into_value() and .into_entry_and_value() -> (OccupiedEntry, V). This can get confusing, as .entry().key() would work, but .entry().remove() would need .into_entry().remove() instead, which would also consume the value.

@@ -470,6 +470,24 @@ impl Error for char::DecodeUtf16Error {
}
}

#[unstable(feature = "map_try_insert", issue = "none")]
impl<'a, K: Debug + Ord, V: Debug> Error
Copy link
Member Author

@m-ou-se m-ou-se Mar 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added Error (and Display) implementations for OccupiedError for completeness, since it's an error. However, since it has non-static lifetimes, the Error implementation is not very useful in most cases.

@rust-log-analyzer

This comment has been minimized.

@Amanieu
Copy link
Member

Amanieu commented Mar 4, 2021

@bors r+

@bors
Copy link
Contributor

bors commented Mar 4, 2021

📌 Commit eddd4f0 has been approved by Amanieu

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 4, 2021
JohnTitor added a commit to JohnTitor/rust that referenced this pull request Mar 5, 2021
Add {BTreeMap,HashMap}::try_insert

`{BTreeMap,HashMap}::insert(key, new_val)` returns `Some(old_val)` if the key was already in the map. It's often useful to assert no duplicate values are inserted.

We experimented with `map.insert(key, val).unwrap_none()` (rust-lang#62633), but decided that that's not the kind of method we'd like to have on `Option`s.

`insert` always succeeds because it replaces the old value if it exists. One could argue that `insert()` is never the right method for panicking on duplicates, since already handles that case by replacing the value, only allowing you to panic after that already happened.

This PR adds a `try_insert` method that instead returns a `Result::Err` when the key already exists. This error contains both the `OccupiedEntry` and the value that was supposed to be inserted. This means that unwrapping that result gives more context:
```rust
    map.insert(10, "world").unwrap_none();
    // thread 'main' panicked at 'called `Option::unwrap_none()` on a `Some` value: "hello"', src/main.rs:8:29
```

```rust
    map.try_insert(10, "world").unwrap();
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
    // OccupiedError { key: 10, old_value: "hello", new_value: "world" }', src/main.rs:6:33
```

It also allows handling the failure in any other way, as you have full access to the `OccupiedEntry` and the value.

`try_insert` returns a reference to the value in case of success, making it an alternative to `.entry(key).or_insert(value)`.

r? `@Amanieu`

Fixes rust-lang/rfcs#3092
m-ou-se added a commit to m-ou-se/rust that referenced this pull request Mar 5, 2021
Add {BTreeMap,HashMap}::try_insert

`{BTreeMap,HashMap}::insert(key, new_val)` returns `Some(old_val)` if the key was already in the map. It's often useful to assert no duplicate values are inserted.

We experimented with `map.insert(key, val).unwrap_none()` (rust-lang#62633), but decided that that's not the kind of method we'd like to have on `Option`s.

`insert` always succeeds because it replaces the old value if it exists. One could argue that `insert()` is never the right method for panicking on duplicates, since already handles that case by replacing the value, only allowing you to panic after that already happened.

This PR adds a `try_insert` method that instead returns a `Result::Err` when the key already exists. This error contains both the `OccupiedEntry` and the value that was supposed to be inserted. This means that unwrapping that result gives more context:
```rust
    map.insert(10, "world").unwrap_none();
    // thread 'main' panicked at 'called `Option::unwrap_none()` on a `Some` value: "hello"', src/main.rs:8:29
```

```rust
    map.try_insert(10, "world").unwrap();
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
    // OccupiedError { key: 10, old_value: "hello", new_value: "world" }', src/main.rs:6:33
```

It also allows handling the failure in any other way, as you have full access to the `OccupiedEntry` and the value.

`try_insert` returns a reference to the value in case of success, making it an alternative to `.entry(key).or_insert(value)`.

r? ``@Amanieu``

Fixes rust-lang/rfcs#3092
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 5, 2021
Rollup of 10 pull requests

Successful merges:

 - rust-lang#80723 (Implement NOOP_METHOD_CALL lint)
 - rust-lang#80763 (resolve: Reduce scope of `pub_use_of_private_extern_crate` deprecation lint)
 - rust-lang#81136 (Improved IO Bytes Size Hint)
 - rust-lang#81939 (Add suggestion `.collect()` for iterators in iterators)
 - rust-lang#82289 (Fix underflow in specialized ZipImpl::size_hint)
 - rust-lang#82728 (Avoid unnecessary Vec construction in BufReader)
 - rust-lang#82764 (Add {BTreeMap,HashMap}::try_insert)
 - rust-lang#82770 (Add assert_matches macro.)
 - rust-lang#82773 (Add diagnostic item to `Default` trait)
 - rust-lang#82787 (Remove unused code from main.js)

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 232caad into rust-lang:master Mar 5, 2021
@rustbot rustbot added this to the 1.52.0 milestone Mar 5, 2021
bors added a commit to rust-lang/hashbrown that referenced this pull request Mar 5, 2021
bors added a commit to rust-lang/hashbrown that referenced this pull request Mar 5, 2021
@m-ou-se m-ou-se deleted the map-try-insert branch March 8, 2021 18:16
@mzji
Copy link

mzji commented Mar 11, 2021

I think we could also have a try_insert_with function, which will call the closure to generate the value only if the key wasn't in the map.

@Blub
Copy link

Blub commented Mar 11, 2021

I know this is easier but I was hoping to see the .entry() API to be more flexible instead (to not require cloning for mere lookups). But I suppose this will cover at least some cases. A _with() variant would definitely cover more ground, too.

@birkett83
Copy link

birkett83 commented May 17, 2021

I think we could also have a try_insert_with function, which will call the closure to generate the value only if the key wasn't in the map.

The motivation for this PR appears to be the ergonomics of error handling when you're assuming there will never be duplicate insertions, i.e. a duplicate insertion indicates there's a bug and you need to panic, log an error message or similar.

If you're not sure whether the key is in the map and want to avoid the cost of generating the value unless it's actually needed, you should probably use the entry API directly, e.g:

map.entry(key).or_insert_with(|| value::new())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-collections Area: `std::collection` S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing a good way to insert into a HashMap while panicking on duplicates
9 participants