How to make two similar structs incompatible, depending on there generic type

Hello,

I want to use the uuid crate for multiple structs (like User, UserGroup...).

Main goal: Even if it's not likely to happen, I want to ensure at compile time that an uuid from the struct User cannot be compared with an uuid from the struct UserGroup.

So the idea is to create a generic type Id<T>, which will trigger an error when trying to compare Id<User> with Id<UserGroup>.

My first try was simply:

struct Id<T> {
    uuid: Uuid,
}

But I corrected it by adding PhantomData:

struct Id<T> {
    uuid: Uuid,
    _1: PhantomData<T>,
}

In order to use it, I also added some derives:

#[derive(Copy, Clone, Eq, PartialEq, Hash)]
struct Id<T> {
    uuid: Uuid,
    _1: PhantomData<T>,
}

But then I got this error when trying to use Id<User> as a HashMap key:

error[E0599]: the method `insert` exists for struct `HashMap<Id<User>, User>`, but its trait bounds were not satisfied
  --> src/main.rs:41:11
   |
10 | pub struct Id<T> {
   | ----------------
   | |
   | doesn't satisfy `Id<User>: Eq`
   | doesn't satisfy `Id<User>: Hash`
...
41 |     users.insert(user.id, user);
   |           ^^^^^^
   |
   = note: the following trait bounds were not satisfied:
           `Id<User>: Eq`
           `Id<User>: Hash`

Here is the whole code:

use std::{
    collections::HashMap,
    hash::{Hash},
    marker::PhantomData,
};

use uuid::Uuid;

#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Id<T> {
    pub uuid: Uuid,
    _1: PhantomData<T>,
}

impl<T> Id<T> {
    fn new() -> Self {
        Self {
            uuid: Uuid::new_v4(),
            _1: PhantomData,
        }
    }
}

pub struct User {
    pub id: Id<User>,
    pub username: String,
}

impl User {
    pub fn new(username: impl Into<String>) -> Self {
        Self {
            id: Id::new(),
            username: username.into(),
        }
    }
}

fn main() {
    let mut users: HashMap<Id<User>, User> = HashMap::new();
    let user = User::new("bob");
    users.insert(user.id, user);
}

The only way I've found to solve the error is to implement manually every trait I need for Id<T>, instead of using the #[derive] attribute:

use std::{
    collections::HashMap,
    hash::{Hash, Hasher},
    marker::PhantomData,
};

use uuid::Uuid;

pub struct Id<T> {
    pub uuid: Uuid,
    _1: PhantomData<T>,
}

impl<T> Clone for Id<T> {
    fn clone(&self) -> Id<T> {
        Id {
            uuid: self.uuid,
            _1: PhantomData,
        }
    }
}

impl<T> Copy for Id<T> {}

impl<T> PartialEq for Id<T> {
    fn eq(&self, other: &Id<T>) -> bool {
        self.uuid == other.uuid
    }
}

impl<T> Eq for Id<T> {}

impl<T> Hash for Id<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.uuid.hash(state);
    }
}

impl<T> Id<T> {
    fn new() -> Self {
        Self {
            uuid: Uuid::new_v4(),
            _1: PhantomData,
        }
    }
}

pub struct User {
    pub id: Id<User>,
    pub username: String,
}

impl User {
    pub fn new(username: impl Into<String>) -> Self {
        Self {
            id: Id::new(),
            username: username.into(),
        }
    }
}

fn main() {
    let mut users: HashMap<Id<User>, User> = HashMap::new();
    let user = User::new("bob");
    users.insert(user.id, user);
}

Is it possible to avoid implementing each basic trait in this situation? Or is there a better way to implement this behaviour (incompatible uuids at compile time) ?

Not currently (derive(Trait) adds a where T: Trait bound).

You might want to make it PhantomData<fn() -> Box<T>> or such to remain Send/Sync.

1 Like

Thank you for your response; you mean that if I keep _1: PhantomData<T> in Id<T>, it looses Send/Sync?

I mean that PhantomData<T> acts like you own a T in terms of auto-traits, so if T: !Send then Id<T>: !Send, similar to the derive situation. So you sometimes lose Send/Sync.

But fn() -> Box<T>: Send + Sync regardless of T.

2 Likes