Essentially adapt @Neutron3529's example. Let's say I want to define a RationalNumber type. Rational numbers can be defined as the equivalence classes formed from the equivalence relation you were taught in grade school. Let's say I don't want to actually "reduce" the ratio of integers during construction though, but I do want to implement Eq such that it adheres to the equivalence relation that typically defines them.
use core::num::NonZeroI64;
#[derive(Clone, Copy, Debug)]
pub struct RationalNumber {
numer: i64,
denom: NonZeroI64,
}
impl PartialEq for RationalNumber {
fn eq(&self, other: &Self) -> bool {
self.numer.wrapping_mul(other.denom.get()) == self.denom.get().wrapping_mul(other.numer)
}
}
impl Eq for RationalNumber {}
#[cfg(test)]
trait TrueEquality {
fn equals(&self, other: &Self) -> bool;
}
#[cfg(test)]
impl TrueEquality for RationalNumber {
fn equals(&self, other: &Self) -> bool {
self.numer == other.numer && self.denom == other.denom
}
}
#[cfg(test)]
mod tests {
use super::{NonZeroI64, RationalNumber, TrueEquality as _};
#[test]
fn true_equality_implies_equivalence() {
const TWO_FOURTHS: RationalNumber = RationalNumber {
numer: 2,
denom: NonZeroI64::new(4).unwrap(),
};
const LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF: RationalNumber = RationalNumber {
numer: 0x3FFF_FFFF_FFFF_FFFF,
denom: NonZeroI64::new(0x7FFF_FFFF_FFFF_FFFE).unwrap(),
};
const THREE_SEVENTHS: RationalNumber = RationalNumber {
numer: 3,
denom: NonZeroI64::new(7).unwrap(),
};
const EIGHT: RationalNumber = RationalNumber {
numer: 8,
denom: NonZeroI64::new(1).unwrap(),
};
const HUNDRED: i64 = 100;
const HUNDRED_ONE: i64 = HUNDRED + 1;
assert_eq!(
TWO_FOURTHS, LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF,
"bug in RationalNumber::eq"
);
assert_eq!(
LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF, TWO_FOURTHS,
"bug in RationalNumber::eq"
);
assert!(
!TWO_FOURTHS.equals(&LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF),
"bug in RationalNumber::equals"
);
assert!(
!LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF.equals(&TWO_FOURTHS),
"bug in RationalNumber::equals"
);
assert_ne!(THREE_SEVENTHS, EIGHT, "bug in RationalNumber::eq");
assert_ne!(EIGHT, THREE_SEVENTHS, "bug in RationalNumber::eq");
assert!(
!THREE_SEVENTHS.equals(&EIGHT),
"bug in RationalNumber::equals"
);
assert!(
!EIGHT.equals(&THREE_SEVENTHS),
"bug in RationalNumber::equals"
);
let mut rational;
for numer in 0i64..=10_000 {
for denom in 1i64..=10_000 {
rational = RationalNumber {
numer,
denom: NonZeroI64::new(denom)
.unwrap_or_else(|| unreachable!("bug in core::num::NonZeroI64::new")),
};
assert!(rational.equals(&rational), "bug in RationalNumber::equals");
assert_eq!(rational, rational, "bug in RationalNumber::eq");
}
}
let mut equal_counter = 0u32;
let mut rational2;
for numer in 0i64..=HUNDRED {
for denom in 1i64..=HUNDRED {
rational = RationalNumber {
numer,
denom: NonZeroI64::new(denom)
.unwrap_or_else(|| unreachable!("bug in core::num::NonZeroI64::new")),
};
for numer2 in 0i64..=HUNDRED {
for denom2 in 1i64..=HUNDRED {
rational2 = RationalNumber {
numer: numer2,
denom: NonZeroI64::new(denom2).unwrap_or_else(|| {
unreachable!("bug in core::num::NonZeroI64::new")
}),
};
if rational.equals(&rational2) {
assert_eq!(rational, rational2, "bug in RationalNumber::eq");
// Can't overflow since `100 * 101 * 100 * 101 < u32::MAX`.
equal_counter += 1;
}
}
}
}
}
assert_eq!(HUNDRED * HUNDRED_ONE, equal_counter.into());
}
}
Summary
Note this is a pathological example. In reality due to how easy it is for bugs to occur from users (including yourself) of your library to elevate equivalence to equality, one should try to strive to implement Eq like it is equality. This is not possible/feasible in a lot of situations though especially in a language like Rust that exposes things like pointers where one can often trivially do something that distinguishes between two entities that are normally in most situations supposed to be "equal". For something as trivial as RationalNumber, it will be used incorrectly even more often due to how something like 1/2 is "equal to" 2/4 is hardwired into your brain; thus I very likely would perform "reduction" in a single pub fn new.