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.
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.
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.
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.
#[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.
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 }?
#[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.
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).
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.
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…)