Broken Equality using Derive Eq in custom types

Not long ago, I encountered a weird case of broken equality checks in my code to which I couldn't find an explanation for.

In a nutshell:

I'm using the NewType pattern to create wrappers around the Ulid type from the ulid crate, which is basically just a wrapper around u128. The Ulid type implements the PartialEq and Eq traits by deriving them, and has tests to support it. For example, creating two Ulids from the same string and comparing them for equality returns true.

The issue I found is that after using some basic build blocks from Rust such as Enums and traits, the equality is broken. I have created a simplified example where I replicate this weird case, hoping to shed some light on the underlying problem:

use std::fmt;
use ulid::Ulid;

fn main() {
    struct MyNewType {
        id: MyOtherNewTypeId,
    }

    #[derive(Debug, Eq, PartialEq, Copy, Clone)]
    struct MyNewTypeId(Ulid);

    impl MyNewTypeId {
        pub fn parse(string: String) -> Result<Self, ()> {
            Ulid::from_string(&string)
                .map(|ulid| Self(ulid))
                .map_err(|_| ())
        }
    }

    impl AsRef<Ulid> for MyNewTypeId {
        fn as_ref(&self) -> &Ulid {
            &self.0
        }
    }

    struct MyOtherNewType {
        pub id: MyOtherNewTypeId,
    }

    #[derive(Debug, Eq, PartialEq, Copy, Clone)]
    struct MyOtherNewTypeId(Ulid);

    impl MyOtherNewTypeId {
        pub fn new() -> Self {
            Self(Ulid::new())
        }

        pub fn parse(string: String) -> Result<Self, ()> {
            Ulid::from_string(&string)
                .map(|ulid| Self(ulid))
                .map_err(|_| ())
        }
    }

    impl AsRef<Ulid> for MyOtherNewTypeId {
        fn as_ref(&self) -> &Ulid {
            &self.0
        }
    }

    #[derive(Debug, Eq, PartialEq)]
    enum MyIdTypesEnum {
        MyNewTypeIdEnumVariant(MyNewTypeId),
        MyOtherNewTypeIdEnumVariant(MyOtherNewTypeId),
    }

    impl From<MyNewTypeId> for MyIdTypesEnum {
        fn from(my_new_type_id: MyNewTypeId) -> Self {
            Self::MyNewTypeIdEnumVariant(my_new_type_id)
        }
    }

    impl From<MyOtherNewTypeId> for MyIdTypesEnum {
        fn from(my_other_new_type_id: MyOtherNewTypeId) -> Self {
            Self::MyOtherNewTypeIdEnumVariant(my_other_new_type_id)
        }
    }

    impl AsRef<Ulid> for MyIdTypesEnum {
        fn as_ref(&self) -> &Ulid {
            match self {
                Self::MyNewTypeIdEnumVariant(my_new_type_id) => my_new_type_id.as_ref(),
                Self::MyOtherNewTypeIdEnumVariant(my_other_new_type_id) => my_other_new_type_id.as_ref(),
            }
        }
    }

    impl fmt::Display for MyIdTypesEnum {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{}", self.as_ref())
        }
    }

    enum MyTypesEnum {
        MyNewTypeEnumVariant(MyNewType),
        MyOtherNewTypeEnumVariant(MyOtherNewType),
    }

    impl MyTypesEnum {
        pub fn get_id(&self) -> MyIdTypesEnum {
            match self {
                Self::MyNewTypeEnumVariant(my_new_type) => my_new_type.id.into(),
                Self::MyOtherNewTypeEnumVariant(my_other_new_type) => my_other_new_type.id.into(),
            }
        }
    }

    struct SomeOtherStruct {
        pub new_types_id: MyIdTypesEnum,
    }

    let some_other_struct = SomeOtherStruct {
        new_types_id: MyIdTypesEnum::MyNewTypeIdEnumVariant(
            MyNewTypeId::parse("01FJ4HS4AQCEBWTGM951G1NHE6".to_string()).unwrap(),
        ),
    };
    let my_types_enum = MyTypesEnum::MyOtherNewTypeEnumVariant(MyOtherNewType {
        id: MyOtherNewTypeId::parse("01FJ4HS4AQCEBWTGM951G1NHE6".to_string()).unwrap(),
    });
    println!(
        "Hello, broken equality! Equality is: {}",
        &some_other_struct.new_types_id == &my_types_enum.get_id()
        // &some_other_struct.new_types_id.to_string() == &my_types_enum.get_id().to_string()
    );
}

If you uncomment the last line and comment out the previous one, then the equality will be restored.

Can't run it on the Playground because ulid is not supported there. Could you please paste the two strings themselves (as opposed to just the boolean result)?

No worries. I found the issue :grinning_face_with_smiling_eyes: . Super rubberduck to the rescue as usual!

The issue is that I'm comparing two different variants of the MyIdTypesEnum enum.

For reference, deriving equality on enums will compare its variants which will return false when you compare different variants. To fix this, the solution is to write a custom implementation of the PartialEq trait. In my case, like this:

impl PartialEq for ItemId {
    fn eq(&self, other: &Self) -> bool {
        self.as_ref() == other.as_ref()
    }
}

Since the enum implements the AsRef trait to return the wrapped NewType struct, and this in turn implements the AsRef trait, which returns the wrapped Uuid type.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.