Skip to content

Conversation

nik-rev
Copy link

@nik-rev nik-rev commented Oct 6, 2025

This RFC proposes that the #[ignore] attribute can now be applied to fields.
Its purpose is to tell derive macros to ignore the field when generating code.

#[derive(Clone, PartialEq, Eq, std::hash::Hash)]
struct User {
    #[ignore(PartialEq, std::hash::Hash)]
    //       ^^^^^^^^^  ^^^^^^^^^^^^^^^
    //       traits that will ignore this field
    name: String,
    #[ignore(PartialEq, std::hash::Hash)]
    age: u8,
    id: u64
}

For the above struct User, derives PartialEq and Hash will ignore the name and age fileds.
Code like this is generated:

impl Clone for User {
    fn clone(&self) -> User {
        User {
            name: self.name.clone(),
            age: self.age.clone(),
            id: self.id.clone(),
        }
    }
}

impl PartialEq for User {
    fn eq(&self, other: &User) -> bool {
        self.id == other.id
    }
}

impl Eq for User {}

impl std::hash::Hash for User {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) -> () {
        std::hash::Hash::hash(&self.id, state)
    }
}

Rendered

@fmease
Copy link
Member

fmease commented Oct 6, 2025

This can and should probably be an ACP, not an RFC.

CC rust-lang/libs-team#334 from >1 year ago which proposed something very similar for Debug. Its implementation had to be blocked because adding new helper attributes to built-in derive macros in core/std is a breaking change (due to lack of "hygienic" or namespaced helper attrs). See link for details.

Similarly, starting to interpret your proposed #[ignore] as a helper attribute is not backward compatible without extending the language (via separate proposals) as alluded to above (not only due to the feature gate error but also due to any validation that's performed).

(We got away with the addition of #[default] for Default either because we were lucky or because it was deemed acceptable at the time (I'd have to dig into the history to find out the specifics) but in general that's not principled and goes against Rust's stability guarantees.)

@nik-rev
Copy link
Author

nik-rev commented Oct 6, 2025

This can and should probably be an ACP, not an RFC.

CC rust-lang/libs-team#334 from >1 year ago which proposed something very similar for Debug. Its implementation had to be blocked because adding new helper attributes to built-in derive macros in core/std is a breaking change (due to lack of hygienic or namespaced helper attrs). See link for details.

Published it as an RFC that is what was suggested in this comment

Validation: The following doesn't compile due to a deny-by-default future incompatibility lint which is going to be promoted to a hard error as part of this RFC:

use derive as ignore;

struct Foo {
    // syntax that will gain meaning because of this RFC
    #[ignore()]
    hello: ()
}

This does not compile because of ambiguity. Whereas if ignore wasn't already built-in, then it would which would be breaking

use derive as ignore;

struct Foo {
    #[ignore]
    hello: ()
}

So then giving this meaning should be fine:

use derive as ignore;

struct Foo {
    #[ignore(Foo, Bar)]
    hello: ()
}
struct Foo {
    #[ignore(Foo, Bar)]
    hello: ()
}

Because it would not have compiled (without turning off the deny-by-default lint which says that we will promote it into a hard error in the future)

This currently does compile, with a warn-by-default lint. And in the RFC I'll clarify that this will continue to compile, and its meaning will not be changed.

struct Foo {
    #[ignore]
    hello: ()
}

I'm not super familiar with terminology, but this isn't a new helper attribute - it's the same attribute #[ignore] that is re-purposed for this proposal.

Summary: If any other name was chosen, then it could be potentially breaking. But because ignore is already a built-in attribute, I think it won't cause breakage aside from upgrading the deny-by-default future incompatibility lint from 6 years ago to an error

If there are any possible breaking changes that I've missed, I would appreciate an example

@fmease
Copy link
Member

fmease commented Oct 6, 2025

Ah, I completely forgot that #[ignore] is of course already a built-in attribute. In that case, it looks like it's indeed backward compatible, thanks for the correction :)

Still, it would "need to become" a helper attribute but that would clash with the built-in attribute, or rather the built-in attribute #[ignore] (used for ignoring tests) would take precedence under the current rules.

However, I'm not an expert in those resolution rules, so maybe it's a non-issue, so I'm happy to be corrected by someone more knowledgeable. I'm wondering if we can properly discern helper and built-in attribute #[ignore] inside ADTs under #[derive]s without some sort of hacks (e.g., not resolving #[ignore] to the built-in attr if it's on a field or variant; there's no precedent for that AFAIK).

@fmease
Copy link
Member

fmease commented Oct 6, 2025

CC https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/namespacing.20macro.20attrs.20to.20reduce.20conflicts.20with.20new.20adds/ (from Jul–Sep '25) (don't quite remember if this is only tangentially related or indeed fully)

Co-authored-by: Jacob Lifshay <programmerjake@gmail.com>
@fmease fmease added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Oct 7, 2025
Copy link
Contributor

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really elegant solution with transforming #[ignore(Xyz)] -> #[ignore]!

@ElchananHaas
Copy link

Does this feature's benefits outweigh implementation complexity?

#[derive(Clone, PartialEq, Eq, std::hash::Hash)]
struct User {
    #[ignore(PartialEq, std::hash::Hash)]
    //       ^^^^^^^^^  ^^^^^^^^^^^^^^^
    //       traits that will ignore this field
    name: String,
    #[ignore(PartialEq, std::hash::Hash)]
    age: u8,
    id: u64
}

This could be replaced with manual implementations of std::hash:Hash, Eq and PartialEq on the id field. This works if there is only 1 or 2 fields used for the trait implementation.

If there is a single field that needs ignoring it can be newtyped and given manual trait implementations. For example

struct OpaqueRng {
    //hidden fields - pretend these don't exist in eq and partialeq comparisons. 
}

impl PartialEq for OpaqueRng {
    fn eq(&self, other: &OpaqueRng) -> bool {
       true
    }
}

now OpaqueRng can be used in any struct without needing this RFC. This covers the case where there is only a few fields that should be ignored.

Are there any motivating cases in the middle where neither approach works well?

@programmerjake
Copy link
Member

Does this feature's benefits outweigh implementation complexity?

I think it does because of how common it is to want to ignore some fields, that imo outweighs all the additional compiler complexity.
I have probably 10 different structs that do that in the codebase I'm currently working on, and that's despite all the extra boilerplate that doing that currently needs.

@kennytm
Copy link
Member

kennytm commented Oct 10, 2025

Currently when you #[derive(PartialEq)] the compiler will automatically implement StructuralPartialEq too (#3535). If any field is #[ignore(PartialEq)]'ed should we still consider the type having structural equality?

@nik-rev
Copy link
Author

nik-rev commented Oct 10, 2025

Currently when you #[derive(PartialEq)] the compiler will automatically implement StructuralPartialEq too (#3535). If any field is #[ignore(PartialEq)]'ed should we still consider the type having structural equality?

If, while deriving PartialEq any field is ignored, then invariants of StructuralPartialEq will not be upheld - so it will not be automatically implemented

66d404d

@ppershing
Copy link

I don't want to start bikeshedding war but for the attribute-syntax shouldn't we move more in the direction of #[derive(ignore=PartialEq)] or maybe even #[derive(PartialEq=ignore, std::cmp::Ord=reversed) in a way similar to e.g. #[serde(rename="new_field_name")]? Attribute scoping is unsolved problem right now and introducing new attributes, especially ones with prominent names such as ignore is going to cause problems as any derive-macro crate will need to consider ever-growing list of "you should not use this attribute" list. Scoping the attributes also helps readers to understand context in which the attribute applies to which is going to be beneficial in case you have multiple 3-rd party derive macros on your struct.

@nik-rev
Copy link
Author

nik-rev commented Oct 14, 2025

Attribute scoping is unsolved problem right now and introducing new attributes, especially ones with prominent names such as ignore is going to cause problems as any derive-macro crate will need to consider ever-growing list of "you should not use this attribute" list

This is not an issue for ignore because it is already a built-in attribute. The RFC describes in details why it won't be a breaking change.

If there are some parts you think could be clarified please let me know!

@ppershing
Copy link

(Sidenote: If you think the discussion is getting out of hand and prefer to have it inside some MR line comments to not clutter the main comment section, please let me know and we can move it there)

You are of course right that it already reserved and so technically no breakage should occur. But the fact that it is used for testing is actually a good reason to not add overload to its meaning!

Also, I really think more discussion should be placed on potential future-proofing. The single #[ignore(trait list)] design precludes all kinds of potentially-useful derive semantics such as "derive comparison in reverse order" or "derive comparison using user_supplied function for this field". Note that these are not "ignore this field" semantics which mean thay would not be reasonable in the attribute namespace - to put bluntly, #[ignore(Ord=reversed)] is definitely a bad idea for future extension as it is unclear what this even means - is the field ignored or not?.

I think the section "How about #[debug(ignore)]" does not provide enough justification for the proposed syntax right now. For starter, it doesn't really explain why we shouldn't adopt the already-existing "api" of derivative crate, just rename it to #[derive(..)] instead of #[derivative(..)]. Why not just copy the established API?

Let me also disagree with the claim "people have to learn each crate's own way of skipping fields" - even with #[ignore] they will at least need to learn whether #[ignore(MyFancyTrait)] actually does something or not. In fact, having #[ignore(list of traits)] actually would make people more tempted to just add MyCustomTrait to the list without even thinking whether it properly supports the #[ignore] attribute. I therefore believe we should have compiler-derived stuff under its own namespace (which could then fully be checked by the compiler).

Going further, similarly to my musings about future derive possibilities, proc macros might have much richer API for handling a given field than just "ignoring" it and trying to split this between this might turn out to be a worse design than "per-crate attribute namespace". For example I definitely would not want to see a mix of #[ignore(serde)], #[ignore(serde(serialize_if_none))] and #[serde(rename=...)] on different fields of the struct and would rather have all serde-related stuff inside its "namespace". The worst, somehow artifficial example I could imagine is for single field have #[ignore(serde(deserialize))] #[serde(rename=...)] to skip field for deserialization but rename for serialization.

Given this I still think attribute format in the form of #[derive(Hash=ignore, Ord=reverse,Eq=user_fn(cmp_lowercase)) is much better in that 1) it leads by an example for attribute scoping, 2) allows for future-proofing, and 3) separates concerns of builtin derive from concerns of other proc macros.

I however see the point in the current fragmentation of ecosystem where some crates use "skip" vs. some "ignore" and might use different syntax. More standardization in this direction should be made but maybe it should not be forced upon you by the language as #[ignore] wants to do. I also acknowledge that multiple attributes per field can get ungainly but as I explained, it is probably better to have each proc macro to have its own namespace.

@nik-rev
Copy link
Author

nik-rev commented Oct 14, 2025

even with #[ignore] they will at least need to learn whether #[ignore(MyFancyTrait)] actually does something or not. In fact, having #[ignore(list of traits)] actually would make people more tempted to just add MyCustomTrait to the list without even thinking whether it properly supports the #[ignore] attribute.

When someone adds a derive to the list of ignored traits that is not supported, if the derive doesn't support the ignore attribute, a compile error is automatically raised that it's not supported. We can do this because a derive must opt-in to attributes(ignore) in order to be able to access the #[ignore] attribute.

But the fact that it is used for testing is actually a good reason to not add overload to its meaning!

If you support the derive attribute on fields, that also overloads the meaning of the derive attribute to mean 2 things:

  • When applied to a struct or enum, derives the trait for the type
  • When applied to a field of a struct or enum that derives the trait, takes a list of arbitrary arguments passed to configure any field-wise derive that applies to this field

Compare the 2 overloads of the ignore attribute:

  • When applied to a function marked with #[test], ignores the function from running in the test suite
  • When applied to a field, allows ignoring derives that apply to the current struct or enum

I think overloading derive has much more potential for confusion because now in a single area you have 1 attribute that means totally different things. While the ignore attribute's 2 meanings are sufficiently far apart and used in completely contexts that I believe there is little potential for them to be confused

Name

Calling this anything-goes configuration attribute derive will be incorrect, because my intuition would tell me that it has something to do with deriving something for the field itself, but in reality the behaviour is completely different - it configures derives instead.

What if we chose a different name, e.g. #[configure_derives]? This would take a list of derives, and arbitrary arguments for each one. At this point, it's not much better than each derive having its own helper methods, but now all of that configuration resides in a single attribute.

Given this I still think attribute format in the form of #[derive(Hash=ignore, Ord=reverse,Eq=user_fn(cmp_lowercase)) is much better in that 1) it leads by an example for attribute scoping, 2) allows for future-proofing, and 3) separates concerns of builtin derive from concerns of other proc macros.

I would suggest to use helper attributes, i.e. #[ignore(Hash)] #[ord(reverse)] #[eq(user_fn(cmp_lowecase))] rather than placing all arguments into a single one such as configure_derive

I think this discussion can be simplified into a single question: Is it worth to have a dedicated attribute ignore for ignoring fields when deriving traits, why can't helper attributes provide this functionality?

The RFC answers this, but as a summary:

  • Without ignore, there is no way that the standard library traits will gain field-wise ignore feature. This is a blocker for them. The 5 standard library traits Hash, PartialEq, Ord, Debug and PartialOrd are the primary use-cases of the ignore attribute.

    We can't add helper attributes for those 5 derives because that would be a breaking change.

  • I also like that #[ignore(Hash, PartialOrd, Ord, PartialEq, Debug)] fits on a single line while something akin to #[hash(ignore)] #[partial_eq(ignore)] #[ord(ignore)] #[debug(ignore)] #[partial_ord(ignore)] would require 5 lines, and be much noisier

  • ignore is a sufficiently universal need for derives that it makes sense to have that as a built-in feature. Something like "use a different function for comparing these 2 fields" is a unique enough need to warrant having individual helper attribute handle this, rather than a single mega-attribute that configures every derive.

  • As you've mentioned, it promotes consistency in code and makes reading Rust code overall less suprising

  • Makes the feature discoverable. If ignore is not supported by a derive, the user will be able to find that out without needing to read any docs.

  • Tooling support

Thank you for taking the time to bring this up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-libs-api Relevant to the library API team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants