Skip to content

Commit

Permalink
Rollup merge of rust-lang#82764 - m-ou-se:map-try-insert, r=Amanieu
Browse files Browse the repository at this point in the history
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
  • Loading branch information
m-ou-se authored Mar 5, 2021
2 parents 68f2934 + eddd4f0 commit 232caad
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 1 deletion.
36 changes: 35 additions & 1 deletion library/alloc/src/collections/btree/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::node::{self, marker, ForceResult::*, Handle, NodeRef, Root};
use super::search::SearchResult::*;

mod entry;
pub use entry::{Entry, OccupiedEntry, VacantEntry};
pub use entry::{Entry, OccupiedEntry, OccupiedError, VacantEntry};
use Entry::*;

/// Minimum number of elements in nodes that are not a root.
Expand Down Expand Up @@ -836,6 +836,40 @@ impl<K, V> BTreeMap<K, V> {
}
}

/// Tries to insert a key-value pair into the map, and returns
/// a mutable reference to the value in the entry.
///
/// If the map already had this key present, nothing is updated, and
/// an error containing the occupied entry and the value is returned.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// #![feature(map_try_insert)]
///
/// use std::collections::BTreeMap;
///
/// let mut map = BTreeMap::new();
/// assert_eq!(map.try_insert(37, "a").unwrap(), &"a");
///
/// let err = map.try_insert(37, "b").unwrap_err();
/// assert_eq!(err.entry.key(), &37);
/// assert_eq!(err.entry.get(), &"a");
/// assert_eq!(err.value, "b");
/// ```
#[unstable(feature = "map_try_insert", issue = "82766")]
pub fn try_insert(&mut self, key: K, value: V) -> Result<&mut V, OccupiedError<'_, K, V>>
where
K: Ord,
{
match self.entry(key) {
Occupied(entry) => Err(OccupiedError { entry, value }),
Vacant(entry) => Ok(entry.insert(value)),
}
}

/// Removes a key from the map, returning the value at the key if the key
/// was previously in the map.
///
Expand Down
35 changes: 35 additions & 0 deletions library/alloc/src/collections/btree/map/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@ impl<K: Debug + Ord, V: Debug> Debug for OccupiedEntry<'_, K, V> {
}
}

/// The error returned by [`try_insert`](BTreeMap::try_insert) when the key already exists.
///
/// Contains the occupied entry, and the value that was not inserted.
#[unstable(feature = "map_try_insert", issue = "82766")]
pub struct OccupiedError<'a, K: 'a, V: 'a> {
/// 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,
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<K: Debug + Ord, V: Debug> Debug for OccupiedError<'_, K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OccupiedError")
.field("key", self.entry.key())
.field("old_value", self.entry.get())
.field("new_value", &self.value)
.finish()
}
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<'a, K: Debug + Ord, V: Debug> fmt::Display for OccupiedError<'a, K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"failed to insert {:?}, key {:?} already exists with value {:?}",
self.value,
self.entry.key(),
self.entry.get(),
)
}
}

impl<'a, K: Ord, V> Entry<'a, K, V> {
/// Ensures a value is in the entry by inserting the default if empty, and returns
/// a mutable reference to the value in the entry.
Expand Down
68 changes: 68 additions & 0 deletions library/std/src/collections/hash/map.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore-tidy-filelength

#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -842,6 +844,37 @@ where
self.base.insert(k, v)
}

/// Tries to insert a key-value pair into the map, and returns
/// a mutable reference to the value in the entry.
///
/// If the map already had this key present, nothing is updated, and
/// an error containing the occupied entry and the value is returned.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// #![feature(map_try_insert)]
///
/// use std::collections::HashMap;
///
/// let mut map = HashMap::new();
/// assert_eq!(map.try_insert(37, "a").unwrap(), &"a");
///
/// let err = map.try_insert(37, "b").unwrap_err();
/// assert_eq!(err.entry.key(), &37);
/// assert_eq!(err.entry.get(), &"a");
/// assert_eq!(err.value, "b");
/// ```
#[unstable(feature = "map_try_insert", issue = "82766")]
pub fn try_insert(&mut self, key: K, value: V) -> Result<&mut V, OccupiedError<'_, K, V>> {
match self.entry(key) {
Occupied(entry) => Err(OccupiedError { entry, value }),
Vacant(entry) => Ok(entry.insert(value)),
}
}

/// Removes a key from the map, returning the value at the key if the key
/// was previously in the map.
///
Expand Down Expand Up @@ -1851,6 +1884,41 @@ impl<K: Debug, V> Debug for VacantEntry<'_, K, V> {
}
}

/// The error returned by [`try_insert`](HashMap::try_insert) when the key already exists.
///
/// Contains the occupied entry, and the value that was not inserted.
#[unstable(feature = "map_try_insert", issue = "82766")]
pub struct OccupiedError<'a, K: 'a, V: 'a> {
/// 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,
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<K: Debug, V: Debug> Debug for OccupiedError<'_, K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OccupiedError")
.field("key", self.entry.key())
.field("old_value", self.entry.get())
.field("new_value", &self.value)
.finish()
}
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<'a, K: Debug, V: Debug> fmt::Display for OccupiedError<'a, K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"failed to insert {:?}, key {:?} already exists with value {:?}",
self.value,
self.entry.key(),
self.entry.get(),
)
}
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, K, V, S> IntoIterator for &'a HashMap<K, V, S> {
type Item = (&'a K, &'a V);
Expand Down
18 changes: 18 additions & 0 deletions library/std/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,24 @@ impl Error for char::DecodeUtf16Error {
}
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<'a, K: Debug + Ord, V: Debug> Error
for crate::collections::btree_map::OccupiedError<'a, K, V>
{
#[allow(deprecated)]
fn description(&self) -> &str {
"key already exists"
}
}

#[unstable(feature = "map_try_insert", issue = "82766")]
impl<'a, K: Debug, V: Debug> Error for crate::collections::hash_map::OccupiedError<'a, K, V> {
#[allow(deprecated)]
fn description(&self) -> &str {
"key already exists"
}
}

#[stable(feature = "box_error", since = "1.8.0")]
impl<T: Error> Error for Box<T> {
#[allow(deprecated, deprecated_in_future)]
Expand Down
1 change: 1 addition & 0 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
#![feature(linkage)]
#![feature(llvm_asm)]
#![feature(log_syntax)]
#![feature(map_try_insert)]
#![feature(maybe_uninit_extra)]
#![feature(maybe_uninit_ref)]
#![feature(maybe_uninit_slice)]
Expand Down

0 comments on commit 232caad

Please sign in to comment.