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.