0

I am struggling to make this work:

trait HasVoice: PartialEq + Eq + Hash {
    fn talk(self: &Self) {}
}

#[derive(PartialEq, Hash, Eq)]
struct Duck {
    name: String,
}

#[derive(PartialEq, Hash, Eq)]
struct Dog {
    breed: String,
}

impl HasVoice for Duck {
    fn talk(self: &Self) {
        println!("duck quack!")
    }
}

impl HasVoice for Dog {
    fn talk(self: &Self) {
        println!("dog bark!")
    }
}

fn test() {
    let duck: Duck = Duck {
        name: "duckie".to_string(),
    };

    let dog: Dog = Dog {
        breed: "labrador".to_string(),
    };

    let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new();

    quack_set.insert(duck);
    quack_set.insert(dog);
}

I am getting:

the trait `HasVoice` cannot be made into an object
`HasVoice` cannot be made into an objectrustcClick for full compiler diagnostic
main.rs(59, 17): for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
Wojciech Owczarczyk
  • 5,595
  • 2
  • 33
  • 55
  • 3
    Nit: Why use this explicit syntax `self: &Self` instead of just `&self`? – cafce25 Dec 30 '22 at 15:27
  • 1
    Does this answer your question? [Trait can't be made Object Safe when Hash + PartialEq are Supertraits](https://stackoverflow.com/questions/71717646/trait-cant-be-made-object-safe-when-hash-partialeq-are-supertraits) – cafce25 Dec 30 '22 at 15:31
  • In Rust you'll often see this done through composition, like `pub enum Voiced { Dog(Dog), Duck(Duck) }`. – tadman Dec 30 '22 at 15:39

1 Answers1

1

TL;DR: You didn't specify how to compare Dogs and Ducks, so the compiler is complaining at you. You have to provide a way for them to be compared.

The solution to this is way more complicated than I was expecting, and probably isn't a good idea. You should just use an enum Voiced { Dog(Dog), Duck(Duck) } instead.

The problem

If you view the full compiler diagnostics, you will see the following:

error[E0038]: the trait `HasVoice` cannot be made into an object
  --> src/lib.rs:39:36
   |
39 |     let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new();
   |                                    ^^^^^^^^^^^^ `HasVoice` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/lib.rs:4:17
   |
4  | trait HasVoice: PartialEq + Eq + Hash {
   |       --------  ^^^^^^^^^ ...because it uses `Self` as a type parameter
   |       |
   |       this trait cannot be made into an object...

You can see there's a problem with PartialEq. But what is it?

What's the big deal with PartialEq?

The problem is that PartialEq is declared as PartialEq<Rhs = Self>, which defaults to the trait PartialEq<Self> if you don't specify a type parameter. In other words, PartialEq by default provides the comparison of a type with itself. Consider the following code:

let duck: &dyn HasVoice = &Duck { /* ... */ };
let dog: &dyn HasVoice = &Dog { /* ... */ };
println!("{}", dog == duck);

What do you expect the outcome to be?

Well, dog == duck is equivalent to dog.eq(duck), right? So Rust will look up the vtable for dog, see that dog does in fact have an eq method of type fn eq(&self, rhs: Dog), and... wait a second. The rhs is explicitly not a Dog, it's a &dyn HasVoice, which contains a pointer to the type Duck. So there's absolutely no way you can call eq on these two types.

This issue was discovered back in 2015, and the decided solution was to ban any Self parameters in supertraits that aren't &self. This means that PartialEq<Self> cannot be used to compare two Box<dyn HasVoice> types.

So then what?

How do I fix this?

Before we know how to fix the issue, we need to know first:

How do we compare Dog and Duck?

Do they compare unequal always? Equal always? Something more exotic? The compiler doesn't know, only you do, so you should specify it. What should the answer be?

Let's say that Dogs and Ducks are always unequal. This is probably what most people expect from dogs and ducks in real life. How do we achieve that?

Well, you can do a bunch of trait machinery manually, but the easiest way is to use the dyn_partial_eq crate.

Here's how:

use dyn_partial_eq::*;

#[dyn_partial_eq]
trait HasVoice {
    fn talk(&self);
}

#[derive(DynPartialEq, PartialEq, Eq, Hash)]
struct Duck {
    name: String,
}

#[derive(DynPartialEq, PartialEq, Eq, Hash)]
struct Dog {
    breed: String,
}

Then PartialEq works properly, right? So all is good?

Well, no. We forgot about Hash.

How do we implement Hash?

The Hash trait is not object-safe. If you add it to the trait requirements, you'll just get a compile error. This is because Hash::hash is a generic function, so it can't be put in a vtable.

We can get around this by making a DynHash trait that isn't generic and instead takes a &mut dyn reference. Here's how you might do it:

trait DynHash {
    fn dyn_hash(&self, state: &mut dyn Hasher);
}

impl<H: Hash + ?Sized> DynHash for H {
    fn dyn_hash(&self, mut state: &mut dyn Hasher) {
        self.hash(&mut state);
    }
}

#[dyn_partial_eq]
trait HasVoice: DynHash {
    fn talk(&self);
}

impl Hash for dyn HasVoice {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.dyn_hash(state)
    }
}

impl Eq for Box<dyn HasVoice> {} // required for HashSet

That's all you need.

The final code

use std::hash::{Hash, Hasher};
use std::collections::HashSet;
use dyn_partial_eq::*;

trait DynHash {
    fn dyn_hash(&self, state: &mut dyn Hasher);
}

impl<H: Hash + ?Sized> DynHash for H {
    fn dyn_hash(&self, mut state: &mut dyn Hasher) {
        self.hash(&mut state);
    }
}

#[dyn_partial_eq]
trait HasVoice: DynHash {
    fn talk(&self);
}

impl Hash for Box<dyn HasVoice> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.dyn_hash(state)
    }
}

impl Eq for Box<dyn HasVoice> {}

#[derive(DynPartialEq, PartialEq, Eq, Hash, Clone)]
struct Duck {
    name: String,
}

#[derive(DynPartialEq, PartialEq, Eq, Hash, Clone)]
struct Dog {
    breed: String,
}

impl HasVoice for Duck {
    fn talk(&self) {
        println!("duck quack!")
    }
}

impl HasVoice for Dog {
    fn talk(&self) {
        println!("dog bark!")
    }
}

fn main() {
    let duck: Duck = Duck {
        name: "an animal".to_string(),
    };

    let dog: Dog = Dog {
        breed: "an animal".to_string(),
    };

    let dog2: Dog = Dog {
        breed: "an animal".to_string(),
    };

    let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new();

    quack_set.insert(Box::new(duck.clone()));
    quack_set.insert(Box::new(dog.clone()));
    quack_set.insert(Box::new(dog2));

    assert_eq!(quack_set.len(), 2);
}

So that's how you implement Java in Rust. :P

For future improvement: The problem with DynHash

If you actually test the hash function we implemented here, you will notice that it doesn't work that well. More specifically, Dog { breed: "the same string".to_string() } and Duck { name: "the same string".to_string() } hash to exactly the same value, because Hash only cares about the stored data, not the unique type. This isn't a problem in theory, but it is quite annoying, because we really should not get hash collisions between these two types that easily. There's probably a way to fix this with a proc macro, similar to dyn_partial_eq, but that's probably not worth doing here.

virchau13
  • 1,175
  • 7
  • 19