rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.96k stars 1.57k forks source link

[RFC] Default field values #3681

Closed estebank closed 2 weeks ago

estebank commented 3 months ago

Allow struct definitions to provide default values for individual fields and thereby allowing those to be omitted from initializers. When deriving Default, the provided values will then be used. For example:

#[derive(Default)]
struct Pet {
    name: Option<String>, // impl Default for Pet will use Default::default() for name
    age: i128 = 42, // impl Default for Pet will use the literal 42 for age
}

Rendered

Tracking:

tmccombs commented 1 month ago

@crumblingstatue but your proposal only addresses one of the reasons listed in the motivation section of this RFC. It doesn't solve the problem of some fields being required and others being optional (and thus the need for builder patterns). It doesn't provide information for third party derive macros.

SOF3 commented 1 month ago

Recapping a relevant discussion on discord, for .. use Default::default() is a very confusing syntax since it violates an assumption a reader might have for Rust code: if an expression appears once in the code (except in macros), its value should only be evaluated once for the current block, and its type should only be evaluated once for each instantiation of the generics of the current function. This syntax would result in Default::default() generating completely different method calls for each field, which would lead to even more cursed code and diagnostics if someone writes for .. use Default::default().foo where foo is a field on different types (exactly what happens if you do it with macros or e.g. C++ templates).

Also, it seems the RFC fails to address what is so bad about builders. TypedBuilder (which is not mentioned in the RFC) already provides as much type safety as default field values can provide without requiring any language change, whilst also providing the ability of restrictions (e.g. fields with pub builder setters but cannot be accessed/mutated after construction). The RFC only states "The builder pattern is quite common in the Rust ecosystem, but as shown above its need is greatly reduced with struct field defaults", but does not explain why we want to reduce its need.

skogseth commented 1 month ago

Also, it seems the RFC fails to address what is so bad about builders. TypedBuilder (which is not mentioned in the RFC) already provides as much type safety as default field values can provide without requiring any language change, whilst also providing the ability of restrictions (e.g. fields with pub builder setters but cannot be accessed/mutated after construction). The RFC only states "The builder pattern is quite common in the Rust ecosystem, but as shown above its need is greatly reduced with struct field defaults", but does not explain why we want to reduce its need.

The RFC does specify the motivation: Builders require additional boilerplate. TypedBuilder seems to be solving similar problems to this, so I think it'd be good to mention it in the RFC. I guess fundamentally the question comes down to whether or not you think this is a problem that warrants a language solution.

PoignardAzur commented 1 month ago

There is no reason why a struct couldn't have multiple sets of defaults.

I think in general, defaults mean "the single set of values people would expect this type to have if they didn't think about it too much". It's an easy Schelling point. If your type has multiple sets of possible defaults, it probably shouldn't have default fields or implement the Default trait.

crumblingstatue commented 1 month ago

If your type has multiple sets of possible defaults, it probably shouldn't have default fields or implement the Default trait.

Well, that is the thing. That there is no language concept for the default field values of a struct. But if it does have sensible defaults, it can implement a trait like Default. But this RFC makes default values for struct fields baked in as a language concept, which is less generally useful than making it ergonomic to define (a single or multiple) set(s) of defaults

crumblingstatue commented 1 month ago

@SOF3

Recapping a relevant discussion on discord, for .. use Default::default() is a very confusing syntax since it violates an assumption a reader might have for Rust code: if an expression appears once in the code (except in macros), its value should only be evaluated once for the current block, and its type should only be evaluated once for each instantiation of the generics of the current function. This syntax would result in Default::default() generating completely different method calls for each field, which would lead to even more cursed code and diagnostics if someone writes for .. use Default::default().foo where foo is a field on different types (exactly what happens if you do it with macros or e.g. C++ templates).

If built-in syntax is confusing, then perhaps it could be a (language built-in) macro instead. It could also just inline an expression without, rather than having to rely on the field types implementing traits. This would make it work in const contexts, etc. It would just be syntax sugar for (the very annoying and ugly) way of writing that expression for every remaining field manually. If it doesn't resolve, a custom diagnostic could easily show which fields it doesn't type check for.

impl MyStruct {
    const fn my_defaults() -> Self {
        Self {
            // I only care about explicitly defining this
            field9: 42,
            // And this
            field13: "Explicitly set",
            init_rest_with!(Default::default()),
        }
    }
}
nikomatsakis commented 1 month ago

@rfcbot reviewed

We discussed this in a design meeting and I am in favor of the big picture. Much of our discussion centered around the Default trait and whether it was ok to have fields that don't have defaults for types that do implement Default (there is a certain inconsistency there). My feeling is that most of the types I work with have multiple "levels" of abstraction, with Foo {} syntax being an "internal constructor" where I might like to have more explicit values to ensure I use the right things, but then to have ::new or ::default methods that use the most commonly needed values or set things up with a good initial value. So to me there is a big difference between the public defaults and the private defaults, in other words, with the the former being a superset of the latter but not necessarily equal.

(I also tend to feel that Default is used for "just gimme some value, probably an empty or 0 one", which is itself a pretty aggressive default.)

rfcbot commented 1 month ago

:bell: This is now entering its final comment period, as per the review above. :bell:

traviscross commented 1 month ago

@rfcbot reviewed

In the meeting, we talked about the question of "what are we suggesting that people do?". I.e., are we suggesting, when we stabilize this, that people should go out of their way to write syntactic defaults for non-mandatory fields even when those match the Default::default() on the field?

I.e., should it be considered best practice to write this...

#[derive(Default)]
pub struct ElevatedPoint {
    pub x: u8 = 0,
    pub y: u8 = 0,
    pub z: NonZero<u8> = unsafe { NonZero::new_unchecked(1) },
}

...since it will allow using the new pretty syntax?

To answer that question, we laid this out:

NM: What are the patterns we want people to be using? I think it is:

  1. Bucket of optional things (no mandatory fields):
    • Your users write Foo { .. } or Foo::default() or Foo { option1, .. }
    • You want to be able to add more options later (but not more mandatory fields)
    • Implementation guidance:
      • to specify default values for every field
      • derive default
      • use non-exhaustive
    • Deriving Default is a SemVer commitment to not later having mandatory fields.
  2. Builder with mandatory fields:
    • Your users write Foo { mandatory1, mandatory2, optional1, .. }
    • You want to be able to add more options later (but not more mandatory fields)
    • Implementation guidance:
      • to specify values for some fields
      • do not implement default (which would be a commitment to having no mandatory)
      • use non-exhaustive with some way to say "all future values with have defaults"
  3. Private constructors (with some defaults) but publicly opaque
    • Public users are expected to use default or other public constructor methods
    • Internally you use Foo { } to specify things precisely but wish to use .. to avoid typing all the fields all the time
    • Implementation guidance:
      • Fields are all private, some of which may have default values
      • You implement Default::default as a "constructor" (similar to new)
  4. Just give me a damn value
    • Need something to store

Antipatterns to be avoided at all costs:

  1. Foo::default() != Foo { .. } compiles (i.e., both are valid, but you get different values)

Implications of the patterns 1-4:

  • Lint or error if you derive with some public fields that lack defaults (but others that have them)
    • This ensures you are either in "bucket of optional things" or "private constructors"

Things I would expect to improve over time:

  • Some lighterweight way to get "give me the type's default" -- people will add proc macros for this, but there should be a way to say it
  • A way to use non-exhaustive to say "all future values will have defaults"

That makes sense to me as a story and as a plan.

PoignardAzur commented 1 month ago
pub z: NonZero<u8> = unsafe { NonZero::new_unchecked(1) },

TIL that unwrap isn't const.

rodrimati1992 commented 4 weeks ago
pub z: NonZero<u8> = unsafe { NonZero::new_unchecked(1) },

TIL that unwrap isn't const.

Unless it gets undone, Option::unwrap will be const in the next stable release (1.83). Before that, one could unwrap an Option with pattern matching and panic, so the only reason to use new_unchecked here is not knowing the alternatives.

Ciel-MC commented 3 weeks ago

Interesting, this also allows pseudo-optional args for functions. But I am a little worried about mixing data structures with code. When I see a literal struct construction, my mind puts it as “simple and free”, when I see a function, my brain goes “something is happening here”, I feel like this kind of goes against the idea of being explicit about when things happen… Am I overthinking?

tmccombs commented 3 weeks ago

my mind puts it as “simple and free”

With the initial expression being constrained to const expressions, most of the work is done at compile time, and at runtime is still simple and almost free

rfcbot commented 3 weeks ago

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

This will be merged soon.

traviscross commented 2 weeks ago

The team has accepted this RFC, and we've now merged it.

Thanks to @estebank for writing this up and pushing it forward, and thanks to all the many people who reviewed this and provided helpful feedback.

For further updates, follow the tracking issue: