-
Notifications
You must be signed in to change notification settings - Fork 153
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
Comments
Note that 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 |
> 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 ( But there's another issue I forgot to mention. We have a And there's another crate
and lookup in this hash by It is not possible to implement So to deal with it we need either:
Not ideal. > If there's a common crate, it should be 1.0 and committed to long-term API stabilityLong term API stability might be an issue.
In the example above, Instead of this:
something like this:
and still be able to pass However, I tried to change the semantics of So in our version of indexmap there's a separate
And using But perhaps there's not much to lose here. What's the worst thing can happen? So I don't know what would be the best. But if such crate is to be added, perhaps better get hashbrown onboard first. |
If there are multiple things that could be compared, how do you maintain the requirement, "The implementor must hash like
I think it's fairly likely that this double-indirection will disappear after inlining, especially since
Using a different source for |
There must be some misunderstanding here. I means, when a key has two pointers like Was just stating the obvious.
I saw it didn't! (In our implementation where everything is marked
I'm not proposing any changes here. I couldn't make better version of |
Sorry, yes, that does indeed makes sense.
Not even in its depths? You don't need the whole thing inlined, but the closest thing to a hotspot there is probably the Anyway, I'll take your point that you saw "measurable whole program perf difference." |
CC @Amanieu |
I think there is an argument for moving There is a slight complication in the case of hashbrown when it is used inside the standard library, but we can make the I don't have any strong opinions on the actual design of |
I have published That could work as a drop-in replacement here, just I also started experimenting with I guess we also lose trait object safety, but I'm not sure if anyone cares... |
I checked, it works even for types declared outside of
I don't remember why I struggled to implement it. |
Careful! This compiles, but it doesn't work with edit: I added a working version of this to my branch. |
Oh, |
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 |
If we take 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 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 |
@JustForFun88 I think you may be misunderstanding "self by value", in that the blanket impl is now 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] |
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 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 I tried to understand why |
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. 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. |
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.
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 |
Well, that's kind of a chicken-and-egg problem, especially that nobody knows about 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 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 crossbeam-rs/crossbeam@master...cuviper:crossbeam:comparable
Sorry, reversing the trait direction isn't exactly small... |
This allows types implementating the `Equivalent` trait to work with other hash table, such as `IndexMap`. See indexmap-rs/indexmap#253.
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.
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
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
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 sameEquivalent
implementation can be implemented for various types without depending on specific use case.IndexMap
can extractEquivalent
into a separate crate and reexport this trait to keepindexmap
backward compatible.The text was updated successfully, but these errors were encountered: