- Feature Name: hashmap_entry_and_then
- Start Date: 2017-09-14
- RFC PR:
- Rust Issue:
Summary
Add an and_then
method to std::collections::hash_map::Entry
to allow mutable access to an occupied entry’s value, before any potential calls to or_insert
.
Motivation
Entry
s are useful for allowing access to existing values in a map while also allowing default values to be inserted for absent keys. The existing API is similar to that of Option
, where or
and or_with
can be used if the option variant is None
.
The Entry
API is, however, missing an equivalent of Option
's and_then
method. If it were present it would be possible to modify an existing entry before calling or_insert
without resorting to matching on the entry variant.
Guide-level explanation
If a value in a HashMap
is required to be modified before inserting a default it is possible to do so using entry
in combination with and_then
and or_insert
. This allows modification of occupied entries to be chained with insertion in the case where the entry is vacant, as demonstrated below:
use std::collections::HashMap;
struct Foo {
new: bool,
}
let mut map: HashMap<&str, Foo> = HashMap::new();
map.entry("poneyland")
.and_then(|e| *e.new = false)
.or_insert(Foo { new: true })
assert_eq!(map["poneyland"].new, true)
Without and_then
we would have to use match
over the two Entry
variants: Occupied
and Vacant
. For example:
use std::collections::HashMap;
use std::collections::hash_map::Entry;
struct Foo {
new: bool,
}
let mut map: HashMap<&str, Foo> = HashMap::new();
match map.entry("poneyland") {
Entry::Occupied(entry) => {
entry.into_mut().new = false;
},
Entry::Vacant(entry) => {
entry.insert(Foo { new: true });
},
}
assert_eq!(map["poneyland"].new, true)
Reference-level explanation
This feature has two constraints:
- The new
and_then
method is designed to work alongside the functionality provided by or_insert
and or_insert_with
, therefore it must return an Entry
so that the methods can be chained.
- It must provide access to the contained value, so that the value can be mutated if necessary.
Reference implementation:
impl<'a, K, V> Entry<'a, K, V> {
pub fn and_then<F>(self, mut f: F) -> Self
where F: FnMut(&mut V)
{
match self {
Occupied(mut entry) => {
f(entry.get_mut());
Occupied(entry)
},
Vacant(entry) => Vacant(entry),
}
}
}
The two constraints could also be satisfied if we require the user provided function to accept an OccupiedEntry
and also return it. The user is then free to call get_mut
on it in the function body in order to obtain a mutable borrow of the entry’s value.
Alternative implementation:
impl<'a, K, V> Entry<'a, K, V> {
pub fn and_then<F>(self, f: F) -> Self
where F: FnOnce(OccupiedEntry<K, V>) -> OccupiedEntry<K, V>
{
match self {
Occupied(entry) => Occupied(f(entry)),
Vacant(entry) => Vacant(entry),
}
}
}
This has the advantage that the closure is FnOnce
as opposed to the more restrictive FnMut
; however, the ergonomics for the user are slightly worse as it would have to be called like so:
map.entry("poneyland")
.and_then(|mut e| { e.get_mut() += 1; e })
.or_insert(42);
Drawbacks
This logic can be implemented by matching on the entry variant, as described in the Guide-level section, and as such could be seen as an unneccessary addition to the API.
Another potential drawback is that this change to the API is specifically intended to solve the problem of mutating an existing value before insertion of defaults, but it is conceivable that newcomers could use it instead of following more obvious patterns. For example:
use std::collections::HashMap;
let mut map: HashMap<&str, u32> = HashMap::new();
map.insert("poneyland", 0)
// Using `and_then` for simple mutation of an entry
map.entry("poneyland").and_then(|e| { *e += 1 });
// as opposed to just using the entry in place
*map.entry("poneyland") += 1;
However, the already good documentation of the hash lib should mitigate this risk.
It is also conceivable that newcomers to the API will attempt to use and_then
and or_insert
out of order, although the fact that or_insert
does not return Entry
should make it obvious how the two are supposed to interact and will result in a compile time error.
Rationale and Alternatives
It is difficult to conceive of an alternative that fits the criteria of being compatible with existing methods (in order to support chaining) and also provide mutable access to the entry’s value. An alternative is provided in the Reference-level section, but is not as ergonomic for the user of the API.
Any alternative implementations would have to provide both mutable access to the held value and return the entry, which are hard to balance due to move semantics.
Unresolved questions
Whether or not this change to the API adds significant value over simply using match
and whether or not there are better alternative implementations that have been overlooked.
Out of scope for this RFC is the porting of other methods from Option
, which may make the Entry
API more user-friendly/flexible.
Also out of scope is the potential for an or_insert
alternative (lets call it or_inplace
, for example) to be implemented that returns Entry
instead of &'a mut V
, which would allow the chaining of and_then
after a call to or_inplace
.