Hello all !
I recently stumbled upon the issue of having a HashMap where the key would also be contained in the object stored. I saw this as an issue because most of my keys were String and since I could not use a reference &str on the object's String to act as a key in the HashMap, I needed to reallocate another String.
Then I saw this topic on SO that gave me the inspiration to replace the HashMap with a HashSet and create my own generic wrapper around object inside that allow me to easily use get() on the HashSet.
Since this is pretty much the first time I do some meta-programming in Rust, I wanted some feedback on the quality of my code, and in particular on the use of trait bounds and PhantomData.
use std::borrow::Borrow;
use std::cmp::{Eq, Ord, Ordering, PartialEq};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ops::Deref;
use std::rc::Rc;
pub trait WithId<K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
{
fn id(&self) -> &K;
}
#[derive(Debug)]
pub struct HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
data: Rc<T>,
phantom: PhantomData<K>,
}
impl<T, K> HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
pub fn from_value(value: T) -> Self {
HashedNode {
data: Rc::new(value),
phantom: PhantomData,
}
}
}
impl<T, K> PartialEq for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
fn eq(&self, other: &Self) -> bool {
return self.data.id().eq(&other.data.id());
}
}
impl<T, K> PartialOrd for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
return self.data.id().partial_cmp(&other.data.id());
}
}
impl<T, K> Eq for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
}
impl<T, K> Ord for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
fn cmp(&self, other: &Self) -> Ordering {
return self.data.id().cmp(&other.data.id());
}
}
impl<T, K> Borrow<K> for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
fn borrow(&self) -> &K {
return &self.data.id();
}
}
impl<T, K> Hash for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
fn hash<G: Hasher>(&self, state: &mut G) {
self.data.id().hash(state);
}
}
impl<T, K> Deref for HashedNode<T, K>
where
K: Hash + PartialEq + Eq + Ord + ?Sized,
T: WithId<K>,
{
type Target = Rc<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
That way I can do this:
#[cfg(test)]
mod test {
use super::*;
#[derive(Debug)]
pub struct Foo {
pub id: String,
pub value: usize,
}
impl WithId<str> for Foo {
fn id(&self) -> &str {
&self.id
}
}
#[test]
fn test() {
use std::collections::HashSet;
let f = Foo {
id: "a".to_owned(),
value: 2,
};
let mut h: HashSet<HashedNode<Foo, str>> = HashSet::new();
h.insert(HashedNode::from_value(f));
let f2: &Rc<Foo> = &h.get("a").unwrap().data;
assert_eq!(f2.value, 2);
}
}
As of now, I am not super happy with the need to call .data after the .get(), but since I used Deref to access the key, I cannot reimplement it to access the Rc wrapped data underneath.
If you have any advices or your think something is wrong, I'll happily take comments