Currently HashMap and HashSet rely on the inherent implementations of the Eq and Hash traits. I propose moving this behaviour to the S (RandomState) parameter. This change is backwards compatible, but improves flexibility and makes it easier to implement custom comparisons.
The current approach as several downsides:
- Customizing comparisons requires implementing a new-type. This is often a clean way to model, it can also be unnecessarily complex. For example just because I want to compare strings case-insensitively, doesn't mean that it's a whole new kind of string.
- If you want to do a borrowed lookup using a dynamically sized type (e.g.
str), you need to either implement yet another new-type around&str, or unsafely implement wrapping ofstr. - The comparison is determined at compile-time. Sometimes it's needed to customize comparisons at runtime, for example culture/collation dependent string comparisons.
Hasherconstrains hashing implementations to a particular iterative approach. This can hurt performance (I've seen several complaints about this, for example)Borrowin the signatures of methods is rather confusing for beginners
As an alternative I'd introduce these traits:
pub trait EqPolicy<T: ?Sized, Q: ?Sized = T> {
fn eq(&self, x: &T, y: &Q) -> bool;
}
pub trait HashPolicy<T: ?Sized> {
fn hash(&self, x: &T) -> u64;
}
The methods of HashMap should then express their constraints in terms of these traits:
impl<K, V, S> HashMap<K, V, S>
where
S: EqPolicy<K> + HashPolicy<K>,
{
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
S: EqPolicy<K, Q> + HashPolicy<Q>;
pub fn insert(&mut self, k: K, v: V) -> Option<V>;
}
This can be made backwards compatible by adding a blanket implementation based on the existing traits.
impl<S, T> HashPolicy<T> for S
where
S: BuildHasher,
T: Hash + ?Sized,
{
fn hash(&self, x: &T) -> u64 {
self.hash_one(x)
}
}
impl<S, K, Q> EqPolicy<K, Q> for S
where
S: BuildHasher,
K: Borrow<Q> + ?Sized,
Q: Eq + ?Sized,
{
fn eq(&self, x: &K, y: &Q) -> bool {
x.borrow() == y
}
}
Playground which contains a compilable example, and demonstrates that that the new constraints are compatible with the old constraints.
I'm not sure what the best name for these traits is. Alternatives could be EqComparer and HashCalculator. Hasher is unfortunately already taken.
Similar changes could be done to BTreeMap/BTreeSet, but would require adding an additional generic parameter for the policy.