Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I implement `Hash` for an enum with a special case?

Tags:

rust

I have this enum:

enum MyEnum {
    Var1,
    Var2(u32),
    Var3(u32, u32),
}

and I want to achieve the following behavior:

let mut set = HashSet::new();
set.insert(MyEnum::Var1);
set.insert(MyEnum::Var1);
set.insert(MyEnum::Var2(1));
set.insert(MyEnum::Var2(2));
set.insert(MyEnum::Var3(1, 1));
set.insert(MyEnum::Var3(1, 1));
set.insert(MyEnum::Var3(2, 1));
set.insert(MyEnum::Var3(2, 2));

println!("set = {:?}", set);
// set = {Var1, Var2(1), Var2(2), Var3(1, 1), Var3(2, 1)}

That is, I want to change the Hash behavior for variant Var3 specifically to depend on its first u32 only.

Importantly, I don't want to exhaustively match each enum variant since I want to retain the default behavior for all other enum variants. Hence, as far as I can tell, these answers are not sufficient for what I am looking for.

The solution I came up with is this:

#[derive(Debug, Eq)]
enum MyEnum {
    Var1,
    Var2(u32),
    Var3(u32, u32),
}

impl PartialEq for MyEnum {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (MyEnum::Var3(a, _), MyEnum::Var3(b, _)) => a == b,
            _ => format!("{:?}", self) == format!("{:?}", other),
        }
    }
}

impl Hash for MyEnum {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            MyEnum::Var3(a, _) => a.hash(state),
            _ => format!("{:?}", self).hash(state)
        }
    }
}

... and I am looking for feedback/better solutions.

like image 419
savx2 Avatar asked Mar 02 '23 11:03

savx2


2 Answers

The derivative crate has been built to provide automatic implementations for standard traits with support for common customizations. With this crate, you can get exactly what you want and looks like this:

use std::collections::HashSet;
use derivative::Derivative; // 2.2.0

#[derive(Debug, Eq, Derivative)]
#[derivative(PartialEq, Hash)]
enum MyEnum {
    Var1,
    Var2(u32),
    Var3(
        u32, 
        #[derivative(PartialEq="ignore")]
        #[derivative(Hash="ignore")]
        u32
    ),
}

fn main() {
    let mut set = HashSet::new();
    set.insert(MyEnum::Var1);
    set.insert(MyEnum::Var1);
    set.insert(MyEnum::Var2(1));
    set.insert(MyEnum::Var2(2));
    set.insert(MyEnum::Var3(1, 1));
    set.insert(MyEnum::Var3(1, 1));
    set.insert(MyEnum::Var3(2, 1));
    set.insert(MyEnum::Var3(2, 2));
    
    println!("set = {:?}", set);
}
set = {Var1, Var3(1, 1), Var3(2, 1), Var2(2), Var2(1)}

See it on the playground.

like image 143
kmdreko Avatar answered Mar 07 '23 20:03

kmdreko


The answer by @kmdreko shows how to use a third party crate to implement the Hash trait. In most cases that would be the preferred way because it will require less code and you won't have to manually ensure that the Hash-Eq contract is not violated.

Just for completeness this is how you could have implemented it manually without a third party crate:

impl Hash for MyEnum {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            MyEnum::Var1 => {
                state.write_u8(1); // use any distinct number as an enum variant identifier
            }

            MyEnum::Var2(v) => {
                state.write_u8(2); // use any distinct number as an enum variant identifier
                v.hash(state); // hash the actual value
            }

            MyEnum::Var3(v, _) => {
                state.write_u8(3);
                v.hash(state);
            }
        }
    }
}

impl PartialEq for MyEnum {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (MyEnum::Var1, MyEnum::Var1) => true,
            (MyEnum::Var2(x), MyEnum::Var2(y)) => x == y,
            (MyEnum::Var3(x, _), MyEnum::Var3(y, _)) => x == y,
            _ => false,
        }
    }
}

impl Eq for MyEnum {}

like image 38
Svetlin Zarev Avatar answered Mar 07 '23 19:03

Svetlin Zarev