Why == needs PartialEq when match does not

Hello,

One might expect comparing two simple values to be simple, but it isn't:

enum X { A, B }

fn it_is_a(x: X) -> bool {
    x == X::A
}

fn main() {
  println!("{}", it_is_a(X::A));
  println!("{}", it_is_a(X::B));
}

The error:

   Compiling playground v0.0.1 (/playground)
error[E0369]: binary operation `==` cannot be applied to type `X`
 --> src/main.rs:4:7
  |
4 |     x == X::A
  |     - ^^ ---- X
  |     |
  |     X
  |
note: an implementation of `PartialEq` might be missing for `X`
 --> src/main.rs:1:1
  |
1 | enum X { A, B }
  | ^^^^^^ must implement `PartialEq`
help: consider annotating `X` with `#[derive(PartialEq)]`
  |
1 + #[derive(PartialEq)]
2 | enum X { A, B }
  |

I know, an enum may be more complex than that, etc. But then, why does this work:

enum X { A, B }

fn it_is_a(x: X) -> bool {
    match x {
        X::A => true,
        X::B => false
    }
}

fn main() {
  println!("{}", it_is_a(X::A));
  println!("{}", it_is_a(X::B));
}

There even is matches!:

enum X { A, B }

fn it_is_a(x: X) -> bool {
  matches!(x, X::A)
}

fn main() {
  println!("{}", it_is_a(X::A));
  println!("{}", it_is_a(X::B));
}

Why can't we simply default x == y, when no PartialEq has been implemented, to be a synonym to matches!(x, y) where this is possible? Thanks!

Because then implementing the trait would change the behavior of ==, which is surprising and would break user code.

2 Likes

If you look around the language, you might notice that there’s essentially no instance at all[1] of any program behavior that is defined as “do this if type Foo implements trait Bar, and do that otherwise”. Traits are usually just enablers, following a pattern such as, “This works if type Foo implements trait Bar, otherwise the program fails to compile.”

Among other things, this has the advantage that adding a trait implementation doesn’t break/change behavior but just enables more new things.


  1. or at least almost no instance at all ↩︎

4 Likes

There are some counterexamples unfortunately:

The code prints "this", but if you comment out impl ThisOrThat for i32, it will print "that".

4 Likes

I was searching for those counterexamples… also method resolution can have this effect.

3 Likes

By the way, I’m not saying this thread is making anywhere near as weird a request, but taken to the extreme, even sensible features of “let’s not error on this, but do some ‘useful’ fallback behavior” can eventually result in rather confusing language design: (Definitely watch this in case you don’t know it altready!)

4 Likes

Yes, but to change the behavior, one would have to implement PartialEq in such a way that X::A == X::A is false, or X::A == X::B is true, which would be even more surprising.

Here's an example of how it would make more things a breaking change instead of just new functionality.

// Introduced in v1.2
pub enum Foo {
    A(i32),
    B(i32),
}

// Introduced in v1.3, doesn't correspond to what `match` does
impl PartialEq for Foo {
    fn eq(&self, other: &Self) -> bool {
        use Foo::*;
        match (self, other) {
            (A(x), A(y)) |
            (A(x), B(y)) |
            (B(x), A(y)) |
            (B(x), B(y)) => x == y,
        }
    }
}
1 Like

It’s explicitly allowed for PartialEq to be irreflexive, and NaN for floating point numbers famously does it.

1 Like

Counter-example: Drop

1 Like

IMO, one shouldn’t even really consider Drop a trait. Sure it uses trait syntax, and you can (even though you never ever should) even write a T: Drop bound, but really it’s just Rust’s syntax for writing destructors.

You cannot call the Drop::drop method manually, implementations must strictly conform to additional rules like being as generic as the type definition, and clearly adding a Drop implementation can sometimes be a breaking change, …

4 Likes

I can't think of anything for Drop where it's observable whether it's implemented or not. Sometimes implementing Drop can cause your code to stop compiling (reverse of PartialEq), but otherwise, it's identical to having an empty Drop impl.

It is allowed? Huh, that surprises me.

That NaN == NaN is false is indeed surprising, and I've felt no other type should do that.

Something like this happens in std for Cow:

    let a: Cow<str> = Cow::Owned(String::from("hello"));
    let b: Cow<str> = Cow::Borrowed("hello");
    assert_eq!(a, b);
4 Likes

I see. Great example

What if we limit it to the case where one side is a constant enum with no fields?

That's why it's "partial" and we also have Eq.

Also, it's "allowed" even for Eq implementors, in the sense that if you rely on reflexiveness for soundness, you're the one in the wrong. One can code up an irreflexive, Eq-implementing type in entirely safe Rust.

(It's "not allowed" in the sense that you would be in the right to do completely arbitrary things so long as they are not UB. This is why the disclaimers on things like Hash are worded how they are.)

2 Likes

That sounds like a rather arbitrary rule. I much prefer uniform rules to special cases. Special cases that work differently are super confusing, as demonstrated by the "Wat" presentation above.

I can also imagine cases where one might want == to work differently in case of parameterless variants, for example:

enum Set {
    Empty,
    ComplicatedRepresentation(...),
}

where the complicated representation may represent an empty set sometimes.

1 Like

Just derive(PartialEq) on an enum like that?

1 Like

The enum may not be yours. Or you want to compare in a unit test and not change the implementation just to make writing the test easier. Or the enum may have branches with fields that do not implement PartialEq.

I see. The Set example is a good one.

I guess I could suggest what about enums where none of the branches have fields, but that would be rather limited (and then, derive(PartialEq) is always possible).

It still pains me to see that x == X::A does not work out of the box, but I can see now why that is so.

Thanks for all the responses!