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 possibleEquivalent
trait implementations in all possible maps adding dependencies on all these map crates - or create wrappers like
HashedKey(Hashed<MyKey>)
,HashedKeyRef(Hashed<MyKeyRef>)
and implementEquivalent
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)
orenum 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 stableequivalent2
crate forEquivalent
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 introducedstruct MyKeyRef(&str, &str)
, andEquivalent
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 inlinedget
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."
CC @Amanieu
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 takesself
by value. ButHash
can't be a trait object either, sodyn 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 theBytesAsStringRef
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 byE
. Or why yourEquivalent
trait extends bothhashbrown::Equivalent
(which takes self by reference) andindexmap::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...