#[cmp(ignore)] attribute for deriving `*Eq`, `*Ord`, `Hash`

In the same vein as allowing a #[default] attribute on enum variants, adding a #[cmp(ignore)] attribute to a field should tell the derive macros for PartialEq, Eq, PartialOrd, Ord, Hash to ignore that field.

Apologies if this has been posted before, I can't think of many good search terms for this.

5 Likes

With deriving PartialEq and Eq, should it also ignore it for structural matching?

Hmm. Why should a cmp attribute influence the Hash impl? My gut feels like it wants a different attribute to say "does not contribute to hashing" (say, an internal cache or something).

There is a relationship that hashes must be equal when the values compare equal. So if a field is ignored for equality, it should also be ignored for hashing.

12 Likes

Ah, fair enough.

A very similar request could be made for #[debug(ignore)] and even #[clone(ignore)] (i.e. use the Default::default() instead of cloning the field). I’ve written quite a few such manual implementations just because a field needed to be ignored.

There is a balance to be struck between complicating the mechanism by making it more customisable and the gain in consistency (this only applies to #[cmp(ignore)]) and readability won by removing the clutter that needs a human brain diff to figure out “oh, this impl exists only to ignore that field — ah, and it is actually correctly done”.

To my mind the benefits outweigh the cost, but I can see how others may come to a different conclusion.

3 Likes

Pattern matching is fixed by the language, not customisable via traits, so I’d find it highly surprising to see a derive macro attribute effecting match behaviour.

Another consideration is that the desire to extract a field’s value is not bound to whether this field semantically is part of its container’s identity.

That's not quite right :slight_smile:

StructuralEq refers to the ability to write

#[derive(PartialEq, Eq)]
struct Thingy(i32, i32);

const THINGY: Thingy = Thingy(0, 1);

let thingy = Thingy(1, 2);

match thingy {
    THINGY => println!("equals the constant"),
    _ => println!("doesn't equal the constant"),
}

Using a constant like this is only valid if the type derives PartialEq, as does all of its fields' types. This is represented in the compiler as a StructuralEq trait, of which an implementation is provided by the derive.

StructuralEq is not provided for custom PartialEq implementations. I'd very much expect a derived PartialEq to not provide StructuralEq either.

1 Like

Oh, I learnt a new feature today, thanks! I had misunderstood “structural matching” to refer to normal deconstruction (to which my points still apply).

I’m not sure if I agree with not providing StructuralEq and StructuralPartialEq if some fields are ignored, if that is what you’re saying: why should x == CONST behave differently from match x { CONST => true, _ => false }?

EDIT: this is incorrect, see comment below (#[cmp(ignore)] attribute for deriving `*Eq`, `*Ord`, `Hash` - #12 by Nemo157)

#[derive(PartialEq, Eq)]
struct Thingy(#[cmp(ignore)] i32, i32);

let thingy = Thingy(1, 2);

match thingy {
    // Should the following match succeed?
    Thingy(99999999, 2) => println!("matches!"),
    _ => println!("doesn't match"),
}

If matching behaves differently from Eq, then that's a potential footgun. But if a pattern that doesn't look like it matches the scrutinee actually does match because of details of the Eq impl, that's also potentially a footgun. Rust currently deals with this by not allowing types with custom Eq impls to do structural match at all.

3 Likes

That specific case isn't using StructuralEq, it's a tuple-struct pattern so it will definitely not match. For StructuralEq you would need to test:

const THINGY: Thingy = Thingy(999999999, 2);
match thingy {
    // Should the following match succeed?
    THINGY => println!("matches!"),
    _ => println!("doesn't match"),
}

(And introducing inconsistencies between struct patterns and const path patterns is another problem).

3 Likes

maybe just an attribute that hides fields from (selectable) derive macros in general? Clone is the first exception, which we cannot hide from.

1 Like

Given that derives get the raw syntax, I'm not sure how that would even work. Userland proc macros get a TokenStream, while the built-in macros get the AST. Either way, hiding something with a mechanism would have quite a bit of overhead (duplicating the entire TokenStream/AST) and too magical for my taste.

@Jules-Bertholet could you elaborate on this?

I did a quick test and this code prints "equal: true, doesn't match"


struct Thingy(i32, i32);

impl std::cmp::PartialEq for Thingy {
    fn eq(&self, other: &Self) -> bool {
        self.1 == other.1
    }
}

impl std::cmp::Eq for Thingy {}

fn main() {
    let thingy = Thingy(1, 2);
    
    print!("equal: {}, ", thingy == Thingy(99999999, 2));
    
    match thingy {
        // Should the following match succeed?
        Thingy(99999999, 2) => println!("matches!"),
        _ => println!("doesn't match"),
    }
}

That is a tuple-struct-pattern, not a structural match. Check this:

1 Like

That’s… actually really confusing given how often const X = Y is explained as basically doing a substitution of X with Y everywhere. I guess patterns are special given their looks-like-expression-but-actually-dual-of-expression nature, but still.

const is a hygienic substitution of the computed value, not of the expression. Also, consider const X = some().complex().expression() instead of Thingy(99999999, 2); you clearly can't just inline an arbitrary expression (as a pattern).

The StructuralEq bound is required specifically such that pattern matching a const is guaranteed structural and not just it if it == CONST.

(Well, as long as constant expressions can't have side effects, referential transparency says it shouldn't actually matter whether you copypaste the expression or its value, but I digress…)

1 Like

I was wrong, as @Nemo157 correctly pointed out.