Closed estebank closed 2 weeks 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.
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.
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 withpub
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 withstruct
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.
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.
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
@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 inDefault::default()
generating completely different method calls for each field, which would lead to even more cursed code and diagnostics if someone writesfor .. use Default::default().foo
wherefoo
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()),
}
}
}
@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.)
:bell: This is now entering its final comment period, as per the review above. :bell:
@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:
- Bucket of optional things (no mandatory fields):
- Your users write
Foo { .. }
orFoo::default()
orFoo { 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.- 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"
- 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 tonew
)- Just give me a damn value
- Need something to store
Antipatterns to be avoided at all costs:
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.
pub z: NonZero<u8> = unsafe { NonZero::new_unchecked(1) },
TIL that unwrap isn't const.
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.
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?
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
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.
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:
Allow
struct
definitions to provide default values for individual fields and thereby allowing those to be omitted from initializers. When derivingDefault
, the provided values will then be used. For example:Rendered
Tracking: