From d156eaf49ea65e13708f8cddd2d09d0a206ec940 Mon Sep 17 00:00:00 2001 From: Pablo Emilio Escobar Gaviria Date: Tue, 24 Mar 2020 16:51:08 -0400 Subject: [PATCH] Implemented `HashMap::try_insert` (#74) If an entry already exists in the map for a given key, the existing `HashMap::insert` replaces it. That is not always what you want. This patch adds `HashMap::try_insert`, which returns an error if the key is already present in the map. --- src/lib.rs | 2 +- src/map.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++----- src/map_ref.rs | 13 +++++-- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 02b64e58..9f1bc685 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,7 +223,7 @@ mod set; /// Iterator types. pub mod iter; -pub use map::HashMap; +pub use map::{HashMap, TryInsertError}; pub use map_ref::HashMapRef; pub use set::HashSet; diff --git a/src/map.rs b/src/map.rs index 84a26b3b..3522c7df 100644 --- a/src/map.rs +++ b/src/map.rs @@ -3,7 +3,8 @@ use crate::node::*; use crate::raw::*; use crossbeam_epoch::{self as epoch, Atomic, Guard, Owned, Shared}; use std::borrow::Borrow; -use std::fmt::{self, Debug, Formatter}; +use std::error::Error; +use std::fmt::{self, Debug, Display, Formatter}; use std::hash::{BuildHasher, Hash, Hasher}; use std::iter::FromIterator; use std::sync::{ @@ -143,8 +144,7 @@ enum PutResult<'a, T> { new: &'a T, }, Exists { - old: &'a T, - #[allow(dead_code)] + current: &'a T, not_inserted: Box, }, } @@ -154,7 +154,7 @@ impl<'a, T> PutResult<'a, T> { match *self { PutResult::Inserted { .. } => None, PutResult::Replaced { old, .. } => Some(old), - PutResult::Exists { old, .. } => Some(old), + PutResult::Exists { current, .. } => Some(current), } } @@ -168,6 +168,38 @@ impl<'a, T> PutResult<'a, T> { } } +/// The error type for the [`HashMap::try_insert`] method. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TryInsertError<'a, V> { + /// A reference to the current value mapped to the key. + pub current: &'a V, + /// The value that [`HashMap::try_insert`] failed to insert. + pub not_inserted: V, +} + +impl<'a, V> Display for TryInsertError<'a, V> +where + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "Insert of \"{:?}\" failed as key was already present with value \"{:?}\"", + self.not_inserted, self.current + ) + } +} + +impl<'a, V> Error for TryInsertError<'a, V> +where + V: Debug, +{ + #[inline] + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + // === // the following methods only see Ks and Vs if there have been inserts. // modifications to the map are all guarded by thread-safety bounds (Send + Sync + 'static). @@ -1334,8 +1366,7 @@ where /// If the map did not have this key present, [`None`] is returned. /// /// If the map did have this key present, the value is updated, and the old - /// value is returned. The key is not updated, though; this matters for - /// types that can be `==` without being identical. See the [std-collections + /// value is returned. The key is left unchanged. See the [std-collections /// documentation] for more. /// /// [`None`]: std::option::Option::None @@ -1362,6 +1393,55 @@ where self.put(key, value, false, guard).before() } + /// Inserts a key-value pair into the map unless the key already exists. + /// + /// If the map does not contain the key, the key-value pair is inserted + /// and this method returns `Ok`. + /// + /// If the map does contain the key, the map is left unchanged and this + /// method returns `Err`. + /// + /// [std-collections documentation]: https://doc.rust-lang.org/std/collections/index.html#insert-and-complex-keys + /// + /// # Examples + /// + /// ``` + /// use flurry::{HashMap, TryInsertError}; + /// + /// let map = HashMap::new(); + /// let mref = map.pin(); + /// + /// mref.insert(37, "a"); + /// assert_eq!( + /// mref.try_insert(37, "b"), + /// Err(TryInsertError { current: &"a", not_inserted: &"b"}) + /// ); + /// assert_eq!(mref.try_insert(42, "c"), Ok(&"c")); + /// assert_eq!(mref.get(&37), Some(&"a")); + /// assert_eq!(mref.get(&42), Some(&"c")); + /// ``` + #[inline] + pub fn try_insert<'g>( + &'g self, + key: K, + value: V, + guard: &'g Guard, + ) -> Result<&'g V, TryInsertError<'g, V>> { + match self.put(key, value, true, guard) { + PutResult::Exists { + current, + not_inserted, + } => Err(TryInsertError { + current, + not_inserted: *not_inserted, + }), + PutResult::Inserted { new } => Ok(new), + PutResult::Replaced { .. } => { + unreachable!("no_replacement cannot result in PutResult::Replaced") + } + } + } + fn put<'g>( &'g self, key: K, @@ -1467,7 +1547,7 @@ where // tree, and the node has been dropped, `value` is the last remaining pointer // to the initial value. return PutResult::Exists { - old: unsafe { v.deref() }, + current: unsafe { v.deref() }, not_inserted: unsafe { value.into_owned().into_box() }, }; } @@ -2506,7 +2586,7 @@ fn no_replacement_return_val() { assert_eq!( map.put(42, String::from("world"), true, &guard), PutResult::Exists { - old: &String::from("hello"), + current: &String::from("hello"), not_inserted: Box::new(String::from("world")), } ); diff --git a/src/map_ref.rs b/src/map_ref.rs index e056c612..c2a8964d 100644 --- a/src/map_ref.rs +++ b/src/map_ref.rs @@ -1,5 +1,5 @@ use crate::iter::*; -use crate::HashMap; +use crate::{HashMap, TryInsertError}; use crossbeam_epoch::Guard; use std::borrow::Borrow; use std::fmt::{self, Debug, Formatter}; @@ -148,12 +148,21 @@ where V: 'static + Sync + Send, S: BuildHasher, { - /// Maps `key` to `value` in this table. + /// Inserts a key-value pair into the map. + /// /// See also [`HashMap::insert`]. pub fn insert(&self, key: K, value: V) -> Option<&'_ V> { self.map.insert(key, value, &self.guard) } + /// Inserts a key-value pair into the map unless the key already exists. + /// + /// See also [`HashMap::try_insert`]. + #[inline] + pub fn try_insert(&self, key: K, value: V) -> Result<&'_ V, TryInsertError<'_, V>> { + self.map.try_insert(key, value, &self.guard) + } + /// If the value for the specified `key` is present, attempts to /// compute a new mapping given the key and its current mapped value. /// See also [`HashMap::compute_if_present`].