indexmap-rs / indexmap

A hash table with consistent order and fast iteration; access items by key or sequence index

Home Page:https://docs.rs/indexmap/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Extract Equivalent trait into a separate crate

stepancheg opened this issue · comments

In our project we have two libraries (one is another implementation of hashmap and another is another implementation of an interner). Both declare own Equivalent trait.

Might be useful is there was a crate which simply declares Equivalent, so the same Equivalent implementation can be implemented for various types without depending on specific use case.

IndexMap can extract Equivalent into a separate crate and reexport this trait to keep indexmap backward compatible.

Note that hashbrown 0.13 added its own copy of Equivalent -- but we wouldn't want to publicly use that here, because that would expose that version as a public dependency in the API.

If there's a common crate, it should be 1.0 and committed to long-term API stability. I'm not sure if there's much justification for such a micro-crate though. Are there many examples of types that manually implement this? (i.e. types not covered by the blanket Borrow+Eq impl).

> Are there many examples of types that manually implement this?

In our project we have a few. Basically each time a key is a struct or enum which contains more than one pointer (struct MyKey(String, String) or enum MyKey { A(String), B(String) }).

But there's another issue I forgot to mention.

We have a Hashed<T> type which caches the key hash (or it could be another generic wrapper). It is placed in common library, let's call it my_proj_util.

And there's another crate my_proj_bin which depends on my_proj_util. my_proj_bin may want to have:

struct MyKey(String, String);

IndexMap<Hashed<MyKey>, u32>

struct MyKeyRef(&str, &str);

and lookup in this hash by Hashed<MyKeyRef>.

It is not possible to implement Equivalent for Hashed outside of my_proj_util crate.

So to deal with it we need either:

  • implement Equivalent<Hashed<K>> for Hashed<Q> for for all possible Equivalent trait implementations in all possible maps adding dependencies on all these map crates
  • or create wrappers like HashedKey(Hashed<MyKey>), HashedKeyRef(Hashed<MyKeyRef>) and implement Equivalent between them

Not ideal.

> If there's a common crate, it should be 1.0 and committed to long-term API stability

Long term API stability might be an issue.

Equivalent might not not implemented for optimal performance.

In the example above, MyKeyRef already contains pointers, and we pass it as &MyKeyRef making it double pointers while we could pass that "equivalent" key by value, not by reference.

Instead of this:

    pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
    where
        Q: Hash + Equivalent<K>,
    { ... }

something like this:

    pub fn get<Q>(&self, key: Q) -> Option<&V>
    where
        Q: Hash + Equivalent<K>,
    { ... }

and still be able to pass &str where the key is String.

However, I tried to change the semantics of Equivalent + get (or our version of IndexMap) and could not make it automatically work for Borrow.

So in our version of indexmap there's a separate get_by_value function:

    // Using `Equivalent` identical to indexmap `Equivalent`
    pub fn get<Q>(&self, key: &Q) -> Option<&V>
    where
        Q: Hash + Equivalent<K> + ?Sized,
    { ... }

    pub fn get_by_value<Q>(&self, key: Q) -> Option<&V>
    where
        Q: Equivalent<K>,
    { ... }

And using get_by_value results is measurable whole program perf difference.

But perhaps there's not much to lose here. What's the worst thing can happen? indexmap will have to switch to new long term stable equivalent2 crate for Equivalent trait.

So I don't know what would be the best.

But if such crate is to be added, perhaps better get hashbrown onboard first.

In our project we have a few. Basically each time a key is a struct or enum which contains more than one pointer (struct MyKey(String, String) or enum MyKey { A(String), B(String) }).

If there are multiple things that could be compared, how do you maintain the requirement, "The implementor must hash like K, if it is hashable." (This seems like more of an issue for the tuple, not the enum.)

Equivalent might not not implemented for optimal performance.

In the example above, MyKeyRef already contains pointers, and we pass it as &MyKeyRef making it double pointers while we could pass that "equivalent" key by value, not by reference.

I think it's fairly likely that this double-indirection will disappear after inlining, especially since get<Q> and its related functions will be monomorphized for your particular instance of Q. There are a lot of abstractions involved, so I can't be sure -- but any change should be accompanied by careful performance measurements.

But perhaps there's not much to lose here. What's the worst thing can happen? indexmap will have to switch to new long term stable equivalent2 crate for Equivalent trait.

Using a different source for Equivalent would be a breaking change in the API. However, the master branch is currently in a 2.0-pre state, as yet unpublished, so if we're going to make any changes, now is the time! Which is not to say that we should definitely make your suggested changes, but we can at least consider it. That's somewhat orthogonal to where Equivalent comes from though.

If there are multiple things that could be compared, how do you maintain the requirement, "The implementor must hash like K, if it is hashable." (This seems like more of an issue for the tuple, not the enum.)

There must be some misunderstanding here.

I means, when a key has two pointers like struct MyKey(String, String), to avoid allocation, another key need to be introduced struct MyKeyRef(&str, &str), and Equivalent between them.

Was just stating the obvious.

I think it's fairly likely that this double-indirection will disappear after inlining

I saw it didn't! (In our implementation where everything is marked #[inline] but not #[inline(always)], -O but no LTO. That was probably due to fact that fully inlined get is quite large, get was called from hundreds places and compiler tried to avoid code bloat.)

any change should be accompanied by careful performance measurements

I'm not proposing any changes here. I couldn't make better version of Equivalent trait: so that Equivalent implementation could be passed by value.

There must be some misunderstanding here.

I means, when a key has two pointers like struct MyKey(String, String), to avoid allocation, another key need to be introduced struct MyKeyRef(&str, &str), and Equivalent between them.

Sorry, yes, that does indeed makes sense.

I think it's fairly likely that this double-indirection will disappear after inlining

I saw it didn't! (In our implementation where everything is marked #[inline] but not #[inline(always)], -O but no LTO. That was probably due to fact that fully inlined get is quite large, get was called from hundreds places and compiler tried to avoid code bloat.)

Not even in its depths? You don't need the whole thing inlined, but the closest thing to a hotspot there is probably the eq callback used in IndexMapCore::get_index_of, and ideally that hashbrown search loop will get inlined. So there might still be extra indirection on the get<Q> entry point, but hopefully not where it matters.

Anyway, I'll take your point that you saw "measurable whole program perf difference."

I think there is an argument for moving Equivalent into a separate crate so that it can be shared between indexmap and hashbrown (and possibly other crates).

There is a slight complication in the case of hashbrown when it is used inside the standard library, but we can make the equivalent dependency optional (but enabled by default) and use an internal Equivalent trait when building with rustc-dep-of-std.

I don't have any strong opinions on the actual design of Equivalent though.

I have published equivalent 0.1.0 as a direct extraction of the current trait. I also added a Comparable trait that extends the idea to ordered keys, like tree maps, but I'm not sure there's anyone thinking about that yet. :)
https://crates.io/crates/equivalent

That could work as a drop-in replacement here, just pub use re-exported, but like I said I want to reach 1.0 first.

I also started experimenting with Equivalent taking self by value, with the blanket impl on &Q to maintain the existing kind of get(&key) lookups. Here's what that looks like for indexmap -- although note the Copy additions needed for repeated comparison, so we might want it as a supertrait Equivalent<K>: Copy for that reason. (Copying &Q is fine!)
master...cuviper:indexmap:equivalent-by-value

I guess we also lose trait object safety, but I'm not sure if anyone cares...

I also started experimenting with Equivalent taking self by value, with the blanket impl on &Q to maintain the existing kind of get(&key) lookups

I checked, it works even for types declared outside of indexmap crate:

use indexmap::{Equivalent, IndexMap};

#[derive(Copy, Clone, Hash)]
struct BytesAsStringRef<'a>(&'a [u8]);

impl<'a> Equivalent<String> for BytesAsStringRef<'a> {
    fn equivalent(self, key: &String) -> bool {
        self.0 == key.as_bytes()
    }
}

#[test]
fn test() {
    let im: IndexMap<String, i32> = IndexMap::new();
    im.get("123");
    im.get(BytesAsStringRef(b"123"));
}

I don't remember why I struggled to implement it.

im.get(BytesAsStringRef(b"123"));

Careful! This compiles, but it doesn't work with derive(Hash) because Hasher::write_str includes an extra byte for prefix-freedom. That's an implementation detail of the unstable method, which could even change between different hashers once it is stabilized.

edit: I added a working version of this to my branch.

I guess we also lose trait object safety, but I'm not sure if anyone cares...

Oh, Hash is a bigger blocker for that anyway, since its methods have the generic H: Hasher parameter.

Can you please explain the last comment? What should work but does not work?

I mean that currently it is possible to form a trait object, like &dyn Equivalent<K>, and we would lose that if the method takes self by value. But Hash can't be a trait object either, so dyn Equivalent<K> isn't useful anyway.

I mean that currently it is possible to form a trait object, like &dyn Equivalent<K>, and we would lose that if the method takes self by value. But Hash can't be a trait object either, so dyn Equivalent<K> isn't useful anyway.

If we take self by value, then abstractions over collections will also not work (at least at first glance it seems impossible to implement the trait below for all three maps).

The point is that I am now developing common traits in order to abstract from different types of containers (see the code below). If you start accepting a key by value rather than by reference, then the trait cannot be implemented for collections::HashMap<K, V, S>.

use std::collections::{self, hash_map};

pub trait Equivalent<K: ?Sized>: hashbrown::Equivalent<K> + indexmap::Equivalent<K> {
    fn equivalent(&self, key: &K) -> bool;
}

impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
where
    Q: Eq,
    K: Borrow<Q>,
{
    #[inline]
    fn equivalent(&self, key: &K) -> bool {
        *self == *key.borrow()
    }
}

pub trait AnyContainerRef<E: ?Sized> {
    type Key;
    type Value;
    type Keys<'a>: Iterator<Item = &'a Self::Key>
    where
        Self: 'a,
        Self::Key: 'a;

    type Values<'a>: Iterator<Item = &'a Self::Value>
    where
        Self: 'a,
        Self::Value: 'a;

    fn cont_len(&self) -> usize;
    fn is_cont_empty(&self) -> bool;
    fn contains_eq(&self, key: &E) -> bool;
    fn get_value(&self, key: &E) -> Option<&Self::Value>;
    fn get_converted<'a, 'b, FV>(&'a self, key: &E) -> Option<FV>
    where
        FV: From<&'b Self::Value>,
        Self::Value: 'b,
        'a: 'b,
    {
        if let Some(value) = self.get_value(key) {
            return Some(value.into());
        }
        None
    }
    fn cont_keys(&self) -> Self::Keys<'_>;
    fn cont_values(&self) -> Self::Values<'_>;
}

impl<K, V, Q, S> AnyContainerRef<Q> for collections::HashMap<K, V, S>
where
    K: Hash + Eq + Borrow<Q>,
    Q: ?Sized + Hash + Eq,
    S: BuildHasher,
{
    type Key = K;
    type Value = V;
    type Keys<'a>  = hash_map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = hash_map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn cont_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_cont_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: &Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn cont_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn cont_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

impl<K, V, Q, S> AnyContainerRef<Q> for hashbrown::HashMap<K, V, S>
where
    K: Hash + Eq,
    Q: ?Sized + Hash + Equivalent<K>,
    S: BuildHasher,
{
    type Key = K;
    type Value = V;
    type Keys<'a>  = hashbrown::hash_map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = hashbrown::hash_map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn cont_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_cont_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: &Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn cont_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn cont_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

impl<K, V, Q, S> AnyContainerRef<Q> for indexmap::IndexMap<K, V, S>
where
    K: Hash + Eq,
    Q: ?Sized + Hash + Equivalent<K>,
    S: BuildHasher,
{
    type Key = K;
    type Value = V;
    type Keys<'a>  = indexmap::map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = indexmap::map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn cont_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_cont_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: &Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn cont_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn cont_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

But extracting the Equivalent trait in a separate crate is a cool idea 😊

@JustForFun88 I think you may be misunderstanding "self by value", in that the blanket impl is now Equivalent<K> for &Q, so the default "value" is still a reference type. But now it can also be a value type like the BytesAsStringRef wrapper above.

The change in your code would look something like this:

diff --git a/src/lib.rs b/src/lib.rs
index b9083a3174cd..7a67d5868de7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -6,21 +6,21 @@ use std::hash::{BuildHasher, Hash};
 use std::collections::{self, hash_map};
 
 pub trait Equivalent<K: ?Sized>: hashbrown::Equivalent<K> + indexmap::Equivalent<K> {
-    fn equivalent(&self, key: &K) -> bool;
+    fn equivalent(self, key: &K) -> bool;
 }
 
-impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
+impl<Q: ?Sized, K: ?Sized> Equivalent<K> for &Q
 where
     Q: Eq,
     K: Borrow<Q>,
 {
     #[inline]
-    fn equivalent(&self, key: &K) -> bool {
+    fn equivalent(self, key: &K) -> bool {
         *self == *key.borrow()
     }
 }
 
-pub trait AnyContainerRef<E: ?Sized> {
+pub trait AnyContainerRef<E> {
     type Key;
     type Value;
     type Keys<'a>: Iterator<Item = &'a Self::Key>
@@ -35,9 +35,9 @@ pub trait AnyContainerRef<E: ?Sized> {
 
     fn cont_len(&self) -> usize;
     fn is_cont_empty(&self) -> bool;
-    fn contains_eq(&self, key: &E) -> bool;
-    fn get_value(&self, key: &E) -> Option<&Self::Value>;
-    fn get_converted<'a, 'b, FV>(&'a self, key: &E) -> Option<FV>
+    fn contains_eq(&self, key: E) -> bool;
+    fn get_value(&self, key: E) -> Option<&Self::Value>;
+    fn get_converted<'a, 'b, FV>(&'a self, key: E) -> Option<FV>
     where
         FV: From<&'b Self::Value>,
         Self::Value: 'b,
@@ -52,7 +52,7 @@ pub trait AnyContainerRef<E: ?Sized> {
     fn cont_values(&self) -> Self::Values<'_>;
 }
 
-impl<K, V, Q, S> AnyContainerRef<Q> for collections::HashMap<K, V, S>
+impl<K, V, Q, S> AnyContainerRef<&Q> for collections::HashMap<K, V, S>
 where
     K: Hash + Eq + Borrow<Q>,
     Q: ?Sized + Hash + Eq,
@@ -92,7 +92,7 @@ where
 impl<K, V, Q, S> AnyContainerRef<Q> for hashbrown::HashMap<K, V, S>
 where
     K: Hash + Eq,
-    Q: ?Sized + Hash + Equivalent<K>,
+    Q: Hash + Equivalent<K>,
     S: BuildHasher,
 {
     type Key = K;
@@ -109,11 +109,11 @@ where
         self.is_empty()
     }
     #[inline]
-    fn contains_eq(&self, eq: &Q) -> bool {
+    fn contains_eq(&self, eq: Q) -> bool {
         self.contains_key(eq)
     }
     #[inline]
-    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
+    fn get_value(&self, key: Q) -> Option<&Self::Value> {
         self.get(key)
     }
     #[inline]
@@ -129,7 +129,7 @@ where
 impl<K, V, Q, S> AnyContainerRef<Q> for indexmap::IndexMap<K, V, S>
 where
     K: Hash + Eq,
-    Q: ?Sized + Hash + Equivalent<K>,
+    Q: Hash + Equivalent<K>,
     S: BuildHasher,
 {
     type Key = K;
@@ -146,11 +146,11 @@ where
         self.is_empty()
     }
     #[inline]
-    fn contains_eq(&self, eq: &Q) -> bool {
+    fn contains_eq(&self, eq: Q) -> bool {
         self.contains_key(eq)
     }
     #[inline]
-    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
+    fn get_value(&self, key: Q) -> Option<&Self::Value> {
         self.get(key)
     }
     #[inline]

@JustForFun88 I think you may be misunderstanding "self by value", in that the blanket impl is now Equivalent<K> for &Q, so the default "value" is still a reference type. But now it can also be a value type like the BytesAsStringRef wrapper above.

The change in your code would look something like this:

I agree that that old version of the trait can take place. But it was my first version. After a little experimenting with it, I realized that it is not convenient. You have to type E every time, which is inconvenient and annoying (why do I need E if I just want to know the size?). So I rewrote the trait. And now not a single test compiles without additional dances. Well, or you need to specify AnyCollectionRef<'s, E = &'s <Self as KeyContain>::Key>, but then every time you need to use HRTB in the function.
(Note that code below won't compile without #257, and without rust-lang/hashbrown#391).

use core::borrow::Borrow;
use core::hash::{BuildHasher, Hash};
use std::collections::{self, hash_map};

pub trait Equivalent<K: ?Sized>: hashbrown::Equivalent<K> + indexmap::Equivalent<K> + Copy {
    fn equivalent(self, key: &K) -> bool;
}

impl<Q: ?Sized, K: ?Sized> Equivalent<K> for &Q
where
    Q: Eq,
    K: core::borrow::Borrow<Q>,
{
    fn equivalent(self, key: &K) -> bool {
        Q::eq(self, key.borrow())
    }
}

pub trait KeyContain {
    type Key;
}

pub trait ValueContain {
    type Value;
}

macro_rules! impl_key_value_for_hash_map_types {
    ($($type_n:ty)*) => ($(
        impl<K, V, S> KeyContain for $type_n {
            type Key = K;
        }

        impl<K, V, S> ValueContain for $type_n {
            type Value = V;
        }
    )*)
}

impl_key_value_for_hash_map_types! {
    collections::HashMap<K, V, S>
    hashbrown::HashMap<K, V, S>
    indexmap::IndexMap<K, V, S>
}

pub trait AnyCollectionRef<E = <Self as KeyContain>::Key>
where
    Self: KeyContain + ValueContain,
{
    type Keys<'a>: Iterator<Item = &'a Self::Key>
    where
        Self: 'a,
        Self::Key: 'a;

    type Values<'a>: Iterator<Item = &'a Self::Value>
    where
        Self: 'a,
        Self::Value: 'a;

    fn collection_len(&self) -> usize;
    fn is_collection_empty(&self) -> bool;
    fn contains_eq(&self, key: E) -> bool;
    fn get_value(&self, key: E) -> Option<&Self::Value>;
    fn get_converted<'a, 'b, FV>(&'a self, key: E) -> Option<FV>
    where
        FV: From<&'b Self::Value>,
        Self::Value: 'b,
        'a: 'b,
    {
        if let Some(value) = self.get_value(key) {
            return Some(value.into());
        }
        None
    }
    fn collection_keys(&self) -> Self::Keys<'_>;
    fn collection_values(&self) -> Self::Values<'_>;
}

impl<K, V, Q, S> AnyCollectionRef<&Q> for collections::HashMap<K, V, S>
where
    K: Hash + Eq + Borrow<Q>,
    Q: ?Sized + Hash + Eq,
    S: BuildHasher,
{
    type Keys<'a>  = hash_map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = hash_map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn collection_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_collection_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: &Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: &Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn collection_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn collection_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

impl<K, V, Q, S> AnyCollectionRef<Q> for hashbrown::HashMap<K, V, S>
where
    K: Hash + Eq,
    Q: Hash + Equivalent<K>,
    S: BuildHasher,
{
    type Keys<'a>  = hashbrown::hash_map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = hashbrown::hash_map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn collection_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_collection_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn collection_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn collection_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

impl<K, V, Q, S> AnyCollectionRef<Q> for indexmap::IndexMap<K, V, S>
where
    K: Hash + Eq,
    Q: Hash + Equivalent<K>,
    S: BuildHasher,
{
    type Keys<'a>  = indexmap::map::Keys<'a, K, V> where Self: 'a, K: 'a, V: 'a;
    type Values<'a>  = indexmap::map::Values<'a, K, V> where Self: 'a, K: 'a, V: 'a;

    #[inline]
    fn collection_len(&self) -> usize {
        self.len()
    }
    #[inline]
    fn is_collection_empty(&self) -> bool {
        self.is_empty()
    }
    #[inline]
    fn contains_eq(&self, eq: Q) -> bool {
        self.contains_key(eq)
    }
    #[inline]
    fn get_value(&self, key: Q) -> Option<&Self::Value> {
        self.get(key)
    }
    #[inline]
    fn collection_keys(&self) -> Self::Keys<'_> {
        self.keys()
    }
    #[inline]
    fn collection_values(&self) -> Self::Values<'_> {
        self.values()
    }
}

#[test]
fn test_one() {
    use hashbrown::HashMap;

    let mut map1: HashMap<String, i32> = HashMap::new();
    // Unfortunately, when calling AnyCollectionRef methods directly,
    // you need to specify the type E
    assert_eq!(AnyCollectionRef::<&String>::collection_len(&map1), 0);
    assert_eq!(AnyCollectionRef::<&str>::collection_len(&map1), 0);

    map1.insert("a".to_owned(), 1);

    let map2: HashMap<usize, usize> = HashMap::from([(1, 1), (2, 2)]);
    // But you do not need to specify the type E when using AnyCollectionRef
    // as trait bound
    fn get_len<T: AnyCollectionRef>(container: &T) -> usize {
        container.collection_len()
    }

    assert_eq!(get_len(&map1), 1);
    assert_eq!(get_len(&map2), 2);
}

#[test]
fn test_two() {
    use hashbrown::HashMap;

    let mut map1: HashMap<String, i32> = HashMap::new();

    // Unfortunately, when calling AnyCollectionRef methods directly,
    // you need to specify the type E
    assert!(AnyCollectionRef::<&String>::is_collection_empty(&map1));
    assert!(AnyCollectionRef::<&str>::is_collection_empty(&map1));

    map1.insert("a".to_owned(), 1);

    let map2: HashMap<usize, usize> = HashMap::from([(1, 1), (2, 2)]);

    // But you do not need to specify the type E when using AnyCollectionRef
    // as trait bound
    fn is_collection_empty<T: AnyCollectionRef>(container: &T) -> bool {
        container.is_collection_empty()
    }

    assert!(!is_collection_empty(&map1));
    assert!(!is_collection_empty(&map2));
}

#[test]
fn test_three() {
    use core::borrow::Borrow;
    use hashbrown::HashMap;

    let map1: HashMap<String, i32> = HashMap::from([("a".into(), 1), ("b".into(), 2)]);

    // You do not need to specify the type E when calling this `AnyCollectionRef`
    // method directly.
    assert!(map1.contains_eq(&"a".to_string()));
    assert!(map1.contains_eq("b"));

    // Also, there is no need to specify the type E when using AnyCollectionRef
    // as a trait bound (although specifying it will give more flexibility).
    fn contain_key<T>(cont: &T, key: &T::Key) -> bool
    where
        T: AnyCollectionRef + ?Sized,
    {
        cont.contains_eq(key)
    }

    fn contain_borrow_key<T, Q>(cont: &T, key: &Q) -> bool
    where
        T: AnyCollectionRef<Q> + ?Sized,
        T::Key: Borrow<Q>,
    {
        cont.contains_eq(key)
    }

    assert!(contain_key(&map1, &"a".to_string()));
    // assert!(contain_key(&map1, "a")); // Err: expected struct `String`, found `str`

    assert!(contain_borrow_key(&map1, &"a".to_string()));
    assert!(contain_borrow_key(&map1, "a"));
}

fn main() {
    println!("Hello, world!");
}

@JustForFun88 can you publish your code as cloneable and compileable repository please?

I tried to copy this snipped from very long the comment into indexmap crate (version with Equivalent by value) tests folder, it does not compile.

I tried to understand why AnyCollectionRef needs to be parameterized by E. Or why your Equivalent trait extends both hashbrown::Equivalent (which takes self by reference) and indexmap::Equivalent (which takes self by value).

@JustForFun88 can you publish your code as cloneable and compileable repository please?

I tried to copy this snipped from very long the comment into indexmap crate (version with Equivalent by value) tests folder, it does not compile.

I tried to understand why AnyCollectionRef needs to be parameterized by E. Or why your Equivalent trait extends both hashbrown::Equivalent (which takes self by reference) and indexmap::Equivalent (which takes self by value).

Sure! Here's the complete latest implementation https://github.com/JustForFun88/collection-traits. There is not only this particular trait, but also others. But the documentation and tests are not finished yet.
I started this whole story to be able to take advantage of the Equivalent trait and at the same time be able to search not only in one HashMap (IndexMap, etc.), but also in the array of HashMaps, that is, there is a default implementation:

impl<E, T, const N: usize> AnyCollectionRef<E> for [T; N]
where
    E: ?Sized,
    T: AnyCollectionRef<E>,
{
    //......
}

I have already finished the implementation, I was just writing tests and documentation, and suddenly such an ambush: the API of the base collections is changing.

@JustForFun88 can you publish your code as cloneable and compileable repository please?

I tried to copy this snipped from very long the comment into indexmap crate (version with Equivalent by value) tests folder, it does not compile.

As for the last code, it won't compile without #257, and without rust-lang/hashbrown#391. I just downloaded these two PRs and specified in cargo.toml the path to these two local repositories. Just wanted to see if my trait could be changed as suggested by @cuviper. It turned out that it is possible and it all compiles, but when it comes to use, everything falls apart. And the beautiful API becomes absolutely inconvenient.

It's certainly a possible outcome that we'll find these more cumbersome, then we probably won't make the change. I'd like to explore if that's inherently in conflict with what you want, or if there's a good middle ground.

and suddenly such an ambush: the API of the base collections is changing.

I purposely labeled these as an experiment -- nothing is changing for certain, and if we can't resolve the concerns that come up, then the bias is toward keeping the status quo.

I purposely labeled these as an experiment -- nothing is changing for certain, and if we can't resolve the concerns that come up, then the bias is toward keeping the status quo.

I apologize if I spoke harshly. Of course, I understand that this is an experiment and, frankly, I'm just happy that I caught it so early and can take part in the discussion of this issue.

I think there's not enough argument for a change, so I'm inclined to leave the trait as-is and publish 1.0. Any final arguments otherwise?

If we need to publish as 1.0, let's avoid big experiments, let's publish as is.

I would postpone publishing Comparable, since there are no (to my best knowledge) users of it. But no harm publishing it too.

I would postpone publishing Comparable, since there are no (to my best knowledge) users of it.

Well, that's kind of a chicken-and-egg problem, especially that nobody knows about equivalent yet. So, challenge accepted, I tried it with crossbeam-skiplist... but I quickly ran into type inference problems, especially with range. That function signature would become:

    pub fn range<'a: 'g, 'g, Q, R>(
        &'a self,
        range: R,
        guard: &'g Guard,
    ) -> Range<'a, 'g, Q, R, K, V>
    where
        R: RangeBounds<Q>,
        Q: Comparable<K> + ?Sized,

Even in simple cases where we would want Q == K, the compiler couldn't guess that from the R type, so almost all of the range test cases had to be turbofished. However, if we flip that around, existing code works fine!

    pub fn range<'a: 'g, 'g, Q, R>(
        &'a self,
        range: R,
        guard: &'g Guard,
    ) -> Range<'a, 'g, Q, R, K, V>
    where
        K: Comparable<Q>,
        R: RangeBounds<Q>,
        Q: ?Sized,

I would flip that around here too, so we'd use K: Equivalent<Q> in constraints. The borrowing direction is the same in the blanket impl, K: Borrow<Q>, and that makes porting to this really easy for existing Borrow + Eq or Borrow + Ord containers -- just change the Borrow to Equivalent/Comparable.

crossbeam-rs/crossbeam@master...cuviper:crossbeam:comparable
master...cuviper:indexmap:flip-equivalent

let's avoid big experiments,

Sorry, reversing the trait direction isn't exactly small...