Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract Equivalent trait into a separate crate #253

Closed
stepancheg opened this issue Jan 17, 2023 · 25 comments · Fixed by #264
Closed

Extract Equivalent trait into a separate crate #253

stepancheg opened this issue Jan 17, 2023 · 25 comments · Fixed by #264

Comments

@stepancheg
Copy link
Contributor

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.

@cuviper
Copy link
Member

cuviper commented Jan 23, 2023

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).

@stepancheg
Copy link
Contributor Author

stepancheg commented Jan 23, 2023

> 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.

@cuviper
Copy link
Member

cuviper commented Jan 24, 2023

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.

@stepancheg
Copy link
Contributor Author

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.

@cuviper
Copy link
Member

cuviper commented Jan 24, 2023

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."

@stepancheg
Copy link
Contributor Author

CC @Amanieu

@Amanieu
Copy link

Amanieu commented Feb 3, 2023

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.

@cuviper
Copy link
Member

cuviper commented Feb 6, 2023

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...

@stepancheg
Copy link
Contributor Author

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.

@cuviper
Copy link
Member

cuviper commented Feb 6, 2023

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.

@cuviper
Copy link
Member

cuviper commented Feb 6, 2023

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.

@stepancheg
Copy link
Contributor Author

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

@cuviper
Copy link
Member

cuviper commented Feb 6, 2023

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.

@JustForFun88
Copy link

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()
    }
}

@JustForFun88
Copy link

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

@cuviper
Copy link
Member

cuviper commented Feb 10, 2023

@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
Copy link

JustForFun88 commented Feb 11, 2023

@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!");
}

@stepancheg
Copy link
Contributor Author

stepancheg commented Feb 11, 2023

@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
Copy link

JustForFun88 commented Feb 11, 2023

@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
Copy link

@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.

@cuviper
Copy link
Member

cuviper commented Feb 11, 2023

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.

@JustForFun88
Copy link

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.

@cuviper
Copy link
Member

cuviper commented Mar 7, 2023

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?

@stepancheg
Copy link
Contributor Author

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.

@cuviper
Copy link
Member

cuviper commented Mar 8, 2023

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...

Amanieu added a commit to Amanieu/hashbrown that referenced this issue Jul 9, 2023
This allows types implementating the `Equivalent` trait to work with
other hash table, such as `IndexMap`.

See indexmap-rs/indexmap#253.
bors added a commit to rust-lang/hashbrown that referenced this issue Jul 9, 2023
Use the `Equivalent` trait from the `equivalent` crate

This allows types implementating the `Equivalent` trait to work with other hash table, such as `IndexMap`.

See indexmap-rs/indexmap#253.
facebook-github-bot pushed a commit to facebook/buck2 that referenced this issue Aug 27, 2023
Summary: A while ago I [asked](indexmap-rs/indexmap#253) indexmap crate author to pull `Equivalent` trait into a crate so we can use it in `starlark_map` and `internment_tweaks`.

Reviewed By: ndmitchell

Differential Revision: D48725109

fbshipit-source-id: 3c06ba4502bc706ca2d2b1b90b0ae12fe9bc0299
facebook-github-bot pushed a commit to facebook/starlark-rust that referenced this issue Aug 27, 2023
Summary: A while ago I [asked](indexmap-rs/indexmap#253) indexmap crate author to pull `Equivalent` trait into a crate so we can use it in `starlark_map` and `internment_tweaks`.

Reviewed By: ndmitchell

Differential Revision: D48725109

fbshipit-source-id: 3c06ba4502bc706ca2d2b1b90b0ae12fe9bc0299
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants