Pre-RFC: User-provided default field values

That's wild. Why would you want to change defaults like that? It seems like a disaster waiting to happen.


I'm not sure if I understand what kind of issue the syntax #[default(...)] / #[default = ...] causes?

In my opinion, it matches the language way better than the proposed syntax.

That's a harsh answer.

I was addressing a question raised earlier in the thread about what if you had a field with a = 1 default, and wanted a different default for some derive macros.

Those would softly imply that this only affects derive(Default), which would be very confusing when it is actually unrelated. The use of field: Type = value does look like let statements, so I wouldn't say that this looks syntactically out of place.

But it can't, even if the feature is implemented as an attribute on the field, this is a new feature enabling something that can't be done today. You can't enforce today that field1 must be user supplied on construction. You can get around this by leveraging the builder pattern.

1 Like

I feel I bit lost. The RFC describes "user-provided default field values". Why would it be unrelated to #[derive(Default)]?

In my opinion, it is actually the best that it looks very related to #[derive(Default)] because it is exactly what it does - configures the output of the derived Default implementation. Other crates could, but don't have to, read this attribute as well.

Sorry, I am assuming familiarity/conflating with the prior discussion at

The idea in that thread evolved to the ability to write

struct Foo {
    mandatory: Type,
    optional: Type = Type::default(),
fn main() {
    let _ = Foo { mandatory: Type::foo(), .. };

which is something that can't be accomplished with Default because you can't have partially initialized values in Rust. Adding this syntax along with the semantics described in the linked post reduces the need of crates writing builders in their APIs. If such feature is added to the language, then leveraging that new syntax from derive(Default) makes sense in my eyes. If partially defaulted values are never adopted, then instead going for a field attribute that derive(Default) consumes and rejecting new syntax would indeed make more sense.


Thank you so much for this context! It makes a lot more sense now.

To be perfectly honest, I'm still not convinced though. I'm not a fan of the different meanings of Foo { .. } vs Foo { ..Default::default() } and the fact that there are two places where the default values could be set - in the struct definition and in the Default::default implementation. I guess this point has been already raised in the pre-pre-RFC you mentioned.

...but it wouldn't be that hard to learn either :slight_smile: ...and perhaps everybody could agree to consider it bad manners to make them different

Responding en masse; no individual part of this response is directed towards a particular user unless someone is directly mentioned.

NB: I have not updated the pre-RFC since its posting. I am discussing what should happen before updating the text, which takes a fair amount of time and effort.

After reading all responses, I am leaning further in favor of the Foo { .. } style initialization present in Centril's RFC and will be responding as such. I suggest others follow suit as that's the direction I'm headed. The RFC text will be updated once there is general agreement on the main principles.

My current intention is to have the default field value be semantically the same as a const block, such that control flow is not supported, everything must be in scope, and the expression is type-checked against the declared field type. With Foo { .. } construction, there is no concern about the expression potentially being dead code. The requirement to be a const context implicitly prohibits async code, though I suppose a block_on call could potentially be supported in const contexts in the future.

With regard to the discussion of how to support different defaults for different derives: why? What situation would necessitate a field to have a different default value depending on what is being derived? And is this a use case we want to encourage, or is is requiring manual implementations or an attribute macro in such a situation (as is the status quo) preferable?

For syn supporting this syntax, it's unfortunate that the library doesn't appear to be forward compatible in this situation. That said, I don't believe we should be making decisions about what to include in a language primarily on the reason that a library can't support it, particularly when said library is capable of creating a breaking release while the language cannot (barring edition-related things). This wouldn't be the first syntax introduced that syn is incapable of handling, as #[foo = expr] currently cannot be parsed. While certainly relevant to the topic at hand, how crates such as syn, serde, etc. would handle the transition (if any) from existing attributes to built-in syntax doesn't seem related to the RFC. Examples are provided in the RFC as to what would theoretically be possible, not what the crates must or even should do; it's a decision for those crates to make on their own.

The syntax for default field values and #[derive(Default)] using them need to land on stable (but not necessarily nightly) at the same time. If syntax lands first, it would be a breaking change to implement the others. Foo { .. } construction would not need to land at the same time. That's why changes to #[derive(Default)] are (and will continue to be) included in the RFC.

One thing I thought of that is not covered in Centril's RFC is that construction of structs where there are private fields (or where the struct is non-exhaustive) is that there needs to be a way to guarantee that future fields have a default value (for external users). I'm not sure what form this should take, so ideas are welcome.

  • is this another change?
  • to allow construction of non_exhaustive structs outside of their crate?

Would it perhaps be better to leave things as they are?

  • non_exhaustive?
  • can't construct form outside, sorry

It's a specific question that is also noted in Centril's RFC. As #[non_exhaustive] is stable, it's a question that will need to be answered as part of this RFC. It's marked as an unresolved question because it's not addressed in the section on interaction with #[non_exhaustive].

Yes, and

is one possible answer. An author who wishes to permit such construction can drop non_exhaustive.

Keep in mind that #[non_exhaustive] was only part of the question I posed. There's still the related question of a struct like

pub struct Window {
    width = 640,
    height = 480,

It needs to be determined how a field without a defaulted value (like foo: T rather than foo: T = bar) can be added to a struct like this in a backwards-compatible manner. Barring an attribute that opts into this syntax, possible solutions likely won't be thought of in a minute or two; it'll need a fair amount of deliberate thought and consideration. Prohibiting situations like this would be extremely restrictive to the point that only exhaustive structs with all fields being public would be permissible — a.k.a. effectively nothing.

FYI, I specifically brought this up mainly, because you mentioned specific crates such as serde in the RFC, and also because the RFC was only about derive-macros at that point. I didn't mean to say that this is a blocker in any form. It might be worth considering the point about syn if we're only talking about adding the = value syntax for usage in derive-macro, because then the feature is only about "hey, let's improve macros", so forcing a breaking change in syn (i.e. not an "improvement" both for macro authors and users of older, unmaintained macros) just for "improving" macros is - at least in principle - a bit questionable. But if the feature of using Foo { .. } construction is also on the table, a breaking change of syn is completely irrelevant for the debate on this language feature and its syntax.

Maybe syn itself could eventually even benefit from this feature, making use of default-initialized fields for structs such as syn::Field in order to avoid further breaking changes when new optional syntax is introduced to the language that requires new fields in such structs.

It was just brought to my attention that there was an RFC that was postponed a few years ago; it appears as though this was actually the basis for Centril's RFC.

This was the crux of that part of my response. I think in any situation it's important to keep crates in mind, but I don't think it should be a significant consideration.

To be perfectly honest, the order of things is coincidental.

Another point that's relevant here, syn itself says that breaking changes are still intended to happen at some point after 1.0

This 1.0 release signifies that Syn is a polished library with a stable design and role and that it offers a user experience we can stand behind as the way we recommend for all Rustaceans to write procedural macros.

Be aware that the underlying Rust language will continue to evolve. Syn is able to accommodate most kinds of Rust grammar changes via the nonexhaustive enums and Verbatim variants in the syntax tree, but we will plan to put out new major versions on a 12 to 24 month cadence to incorporate ongoing language changes as needed.



I think I finally understood: the language needs three kinds of structs:

  1. cannot be constructed outside own crate so that

    • new private or
    • pub fields w/o defaults can be added
      and this is not a breaking change
  2. can be constructed outside own crate
    using the current S{a : b} syntax
    but adding new fields is a breaking change

  3. can be constructed outside own create
    using the new S{a : b, ..} syntax
    but adding new fields without defaults is a breaking change

Currently Rust only has (1) and (2) which are distinguished via non_exhaustive. But there is no way around it is there? Kind number (3) has to be provided, right? And it has to be marked somehow.

1 Like

As I described above, in my view #[non_exhaustive] should be equivalent to having a private field without default value. So #[non_exhaustive] struct Foo {} or #[non_exhaustive] struct Bar { bar: i32 = 0 } does not permit being constructed via Foo { .. } or Bar { .. } or Bar { bar: 2, .. } outside of the crate that defines the type.

One reason: Currently a #[non_exhaustive] pub struct Foo {} is basically equivalent to a pub struct Foo { pub(crate) _dummy: () }; external users of the crate cannot construct this struct through its constructor. This fact can - in principle - even be relied on for soundness.

Following my argument above, the way to do this would then to add #[non_exhaustive] - or equivalently - a private field without default value to the struct.

Either that, or by the presence of private fields.

Not necessarily marked by an attribute though; it could just be:

All structs that aren't #[non_exhaustive] and where all private fields have default values, but there is at least one private field (otherwise we're back in case (1)).

AFAICT; the main benefit of #[non_exhaustive] on a struct is that you don't need to have the overhead of explicitly initializing the otherwise necessary private pub(crate) _marker: () field. However when you'd add a private _marker: () = () field to get your struct from kind number (2) to kind number (3), then the overhead is just that you'll have to initialize it using S{a : b, ..} syntax yourself, too. Far less syntactical overhead.


All your points are true. Explicit marking however might

  • enhance learnability
  • improve error messages
  • lower cognitive load
1 Like

I smell moving goalposts here. I am talking about providing defaults, and not about enforcing non-defaults.

From a bird's eye view, there is a more general pattern emerging here, too. People are apparently trying to jam so much arbitrary, diverse (should I call it incoherent?), and niche semantics into a single feature that it's becoming hard to see how throwing in everyone's needs and trying to please them all would work.

If one has complex requirements regarding the initialization of fields, then express the requirements explicitly in code. Make every field private, implement Default manually if applicable, or don't implement it at all, and provide a constructor which requires all non-defaultable fields to be passed.

Don't try to force the semantics of arbitrarily complex initialization into the territory of "defaults". "Fields are required but not all of them, but we have a kinda-sorta default for some of them some of the time, and there is a Default impl that may or may not make sense semantically, and sometimes I really want a Builder" is a really, really poor design for Default.

Default was meant to be simple, like, "solves 95% of baseline problems with a derive macro" simple. But beyond that, there is the rest of the entire Rust language at everyone's disposal, too! Honestly, I simply cannot fathom why it is not good enough to derive Default (with explicit attributes on fields) in the majority of the cases, and then use the full power of the language when it gets more complicated than that. We have struct construction and FRU syntax, we have privacy, we have functions, we have methods, we have macros – what do they not solve beyond trivial shortening of 3 lines of code to 1 line?