Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 19 of 26

Medium Primitives

PartialEq and Eq: why floats can be compared but not Eq

Deduplicate a list of keys, keeping first-seen order, and flag any key whose entries disagree.

pub fn dedup_keys(keys: Vec<String>, weights: Vec<f64>) -> Vec<String>

keys[i] and weights[i] form one Weighted record. Return each distinct key once, in the order it first appeared. If that key appears more than once with different weights, append a !.

keys    = ["a", "b", "a"]
weights = [1.0, 2.0, 1.0]   ->  ["a", "b"]
weights = [1.0, 2.0, 3.0]   ->  ["a!", "b"]

Two traits, one method between them

pub trait PartialEq<Rhs = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}

pub trait Eq: PartialEq<Self> {}     // no methods at all

Eq is empty. It adds no behaviour whatsoever. It is a promise: that your equality is a genuine equivalence relation —

  • reflexive: a == a for every a
  • symmetric: a == b implies b == a
  • transitive: a == b and b == c implies a == c

PartialEq promises only the last two. This is the first place in the course where Rust encodes a mathematical law in the type system, with a trait that contains no code at all, and it is worth pausing on how odd and how useful that is.

Why f64 is PartialEq and not Eq

f64::NAN != f64::NAN. That is IEEE-754, not a Rust quirk. Reflexivity fails, so floats are only partially equal, so f64 implements PartialEq and deliberately does not implement Eq.

From that one fact everything else follows. HashMap and HashSet require their key to be Eq + Hash, so you cannot use an f64 as a hash key — not because Rust is being difficult, but because a key that is not equal to itself cannot be looked up. BTreeMap requires Ord, whose supertrait is Eq, so floats are out there too.

The starter makes the mistake for you. It derives PartialEq, Eq, Hash on a struct containing an f64, and you get two errors at once:

error[E0277]: the trait bound `f64: Eq` is not satisfied
error[E0277]: the trait bound `f64: Hash` is not satisfied

A single line of derive, two distinct impossibilities, both named precisely.

Note also: if you try #[derive(Eq)] without PartialEq, you get “can’t compare A with A“. Eq is a supertrait of PartialEq, not a replacement for it — you always need both, and Eq is the extra promise on top.

Eq is a promise the compiler cannot check

Nothing stops you writing impl Eq for Weighted {} by hand on a float-containing type. It compiles. It is also a latent bug: a HashMap keyed on it will behave unpredictably the first time a NaN arrives. The compiler enforces that you claimed the property, not that you have it.

Derived PartialEq on an enum

Worth knowing: derived PartialEq compares the discriminant first. Two different variants are never equal no matter what their payloads say — Shape::Circle(1.0) != Shape::Square(1.0) — and identical variants then compare field by field.

Lints in this family

partialeq_ne_impl fires if you override ne. The default !self.eq(other) is almost always what you want, and an override is nearly always a mistake.

derived_hash_with_manual_eq is a deny-by-default correctness lint and the subject of its own item later in this track.

derive_partial_eq_without_eq (allow-by-default) nudges you to add Eq where you could have — with the float exception you now understand.

Remember the grade is compile + tests + clippy -D warnings.