Musings on `PartialEq`

PartialEq docs say that it corresponds to partial equivalence relations.

However, the conditions that it actually requires only have a superficial similarity to partial equivalence relations. Properties that are true for all partial equivalence relations do not follow from PartialEq requirements.

For example:

enum A {
    A1,
    A2,
}

enum B {
    B1,
    B2,
}

impl PartialEq<B> for A {
    fn eq(&self, other: &B) -> bool {
        match (self, other) {
            (A::A1, B::B1) => true,
            (A::A1, B::B2) => true,
            (A::A2, B::B1) => true,
            (A::A2, B::B2) => false,
        }
    }
}

This satisfies all the stated requirements of PartialEq. But it's very much inconsistent with the concept of partial equivalence relations. A1 = B1, A1 = B2 and A2 = B1, which for partial equivalence relations would imply that A2 = B2. But we have A2 ≠ B2.

Are the stated properties of PartialEq actually used by any code? If not, maybe it would make sense to simply remove these requirements altogether, and only require them for Eq?

1 Like

Right, the trait places no requirement for symmetric or transitive impls to exist, only on what the existing ones do. Given that your particular example only has the (A,B)-impl, there are effectively no requirements on what it does.

I don't think that PartialEq between two different types makes much sense unless the types are PartialEq with themselves.

And, given the above definition, there can be no such implementations, since they will break transitivity: A1=B1, B1=A2 implies A1=A2, but then A2=A1, A1=B2 implies A2=B2 which is a contradiction.

In fact, if you use the defaulted ne(), any implementation will be valid in this case. The other requirements, transitivity and symmetry, require more than one trait implementation, and are otherwise trivially fulfilled.

You are both right.

The crux of the issue is that partial equivalence relations is a concept defined for homogeneous relations, whereas PartialEq attempts to apply it to heterogeneous relations.

The proper way to apply it to the heterogeneous case would be to insist that if PartialEq<B> for A, then also PartialEq<A> for B, and if PartialEq<B> for A and PartialEq<C> for B then PartialEq<C> for A.

But that is not practical, especially the transitivity.

The way it is, I don't see any practical value from having these half baked requirements between different types.

I propose that they be simply dropped. PartialEq would become a way to overload == without any such requirements.

Another way to resolve this would be to say that these properties only hold for PartialEq<T> for T, but not between different types.

Technically both solutions would break generic code that depends on these properties and doesn't assume Eq. But I think all the code in std that depends on this (such as HashMap) assumes Eq.

1 Like

PartialOrd takes a different approach:

Note that these requirements mean that the trait itself must be implemented symmetrically and transitively: if T: PartialOrd<U> and U: PartialOrd<V> then U: PartialOrd<T> and T: PartialOrd<V>.

This seems very impractical. It means that if types A and B are implemented by totally different crates, and both implement PartialOrd<i32>, say, then the types A and B must also be comparable against each other. But the authors of these crates may not even know about each other!

1 Like

I have found an open ticket about this.

1 Like

I don't think your categorization of homogenous and heterogenous is entirely relevant. Just because they are different types in rust does not mean that they are different things conceptually. E.g. it could make sense to compare Integers and Rational numbers, or u8 and u16. In these cases the rust types are some subset of some ideal homogenous type.

If this is not the case than when would ever a reasonable implememtation give equality? Apples and oranges should never be equal. Maybe, this could be useful for PartialOrd, where you just want some type to always be greater/lesser than some other unrelated type, and you would then need a PartialEq super trait which always returns false. In this case I guess you have the heterogenous case.

I also don't see any reason why shifting the problem to Eq or Ord would solve any of the issues.

What is slightly aggravating is, as you say, the difference in how PartialEq and PartialOrd handle transitive requirement. The linked ticket seems like it might resolve that.

Even more aggravating though are the semver implications of all possible transitive impls in downstream crates limiting the impls you may actually write, which is quite head-scratching. This is also discussed in the ticket above.

I agree to an extent -- it's workable for different types. Then you can say you have a relation on the union of all these different types. But that only really works if comparisons between all the different types concerned exist.

It is not a problem for Eq and Ord because those only allow comparisons within the same type.