-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Default field values #3681
Conversation
866b2c7
to
43fea8c
Compare
43fea8c
to
16f8bfd
Compare
16f8bfd
to
d81136d
Compare
text/0000-default-field-values.md
Outdated
} | ||
``` | ||
|
||
These can then be used in the following way with the Functional Record Update syntax, with no value: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we should call this FRU, since there's no base object.
My preference would be to say this RFC doesn't actually touch FRU at all, just expands struct field expressions (and the derives and such)...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's useful if the RFC draws parallel to the FRU syntax whenever possible, to avoid needing to re-explain things like how struct initializer expressions are type checked (especially bc going into the detail of how it actually works means this RFC is gonna get it wrong). But it's worthwhile calling this something different than FRU.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree in the reference section, C-E, but not in the summary.
text/0000-default-field-values.md
Outdated
Because this RFC gives a way to have default field values, you can now simply | ||
invert the pattern expression and initialize a `Config` like so (15): | ||
|
||
```rust | ||
let config = Config { width, height, .. }; | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this RFC actually makes this possible.
It would need an additional feature like allowing
#[non_exhaustive(but_future_fields_will_have_defaults)]
pub struct Config {
pub width: u16,
pub height: u16,
}
because #[non_exhaustive]
is a license to add private fields without defaults today.
And there are types today like
#[non_exhaustive]
pub struct Foo;
that we really don't want to make creatable by Foo { .. }
in other crates, since the reason they have that attribute today is to prevent such construction.
I think that's fine, though: this RFC is useful without such an addition, and thus it can be left as future work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to see if I get you right, you saying that this RFC should disallow #[non_exhaustive]
and default fields on the same struct, and leave any interaction as an open question? I'm down with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that defaults would still be allowed, but the type would remain unconstructable from outside the defining crate. That way you can still use things like a default on PhantomData
on internal, non_exhaustive
types.
Given that
#[non_exhaustive]
pub struct Foo;
is commonly used to write a type that cannot be constructed outside the crate (replacing the old pub struct Foo(());
way of writing this), I don't think we can ever say that any non_exhaustive
types are constructible outside the defining crate without the defining crate opting-in to that somehow. Telling everyone they have to change back to having a private field is a non-starter, in my opinion.
But I'd also be fine with saying that you just can't mix them yet, and make decisions about it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I think it would be useful to preserve the property that Struct { field: value, .. }
as an expression (the proposed syntax) is equivalent to Struct { field: value, ..Default::default() }
, and as such, these examples would only work if these structs derived or manually implemented Default
.
That should cover the API concerns, although it would make Default
become a lang item, which I am personally fine with but I am not everyone, so, that would be a downside to this approach.
If it doesn't interact with Default
at all, I agree that it shouldn't allow this, since it does break APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think requiring Default
is unnecessarily limiting, since it would prevent using the nice new syntax with structs where some fields have defaults and others intentionally do not, e.g. if syn::Index
(a tuple struct field name) used the new syntax it could be:
pub struct Index {
pub index: u32,
pub span: Span = Span::call_site(),
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can get pretty far with linting here.
A lint can check if the body of your Default::default
impl is const fn
-compatible, and suggest changing it to have inline defaults if so.
One thing I like about this feature is that it means that the vast majority of (braced-struct) Default
s move to being derive
able -- even Vec
's Default
could, for example! -- and thus anything that's not would start to really stand out, and starting being a candidate for IDE hints and such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rereading this I believe that the part of the feature that has a bad interaction is allowing the construction of types that have private default field values. Disallowing that in general, or when the type is also annotated with non_exhaustive
would eliminate the issue for every cross-crate case, right? The only potential issue would be intra-crate, and that's already out of scope of non_exhaustive
, IIUC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On reading the RFC, I was honestly a little confused on why interaction with non_exhaustive would be a problem.
My understanding is that non_exhaustive means you can't construct the struct outside of the crate. The ability to have defaults shouldn't have any impact on that. And prohibiting use of defaults on a non_exhaustive struct seems unnecessarily restrictive, since that could still be useful within the crate.
Now, there may be value in a non_exhaustive_but_future_fields_have_defaults functionality, but I think that should be a separate attribute, or add an argument to the existing non_exhaustive attribute, not usurp and change the meaning of the current non_exhaustive attribute.
text/0000-default-field-values.md
Outdated
In particular, the syntax `Foo { .. }` mirrors the identical and already | ||
existing pattern syntax. This makes the addition of `Foo { .. }` at worst | ||
low-cost and potentially cost-free. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One drawback that comes to mind is that it'll mean that a pattern Foo { .. }
can match more things than just the expression Foo { .. }
, because the pattern matches any value of the unmentioned fields, but the expression sets them to a particular value.
That means that, with the unstable inline_const_pat
, the arm const { Foo { .. } } =>
matches less than the arm Foo { .. } =>
(assuming a type like struct Foo { a: i32 = 1 }
).
I think I'm probably fine with that, but figured I'd mention it regardless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is an excellent call-out of a non-obvious interaction I hadn't accounted for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be worth adding syntax alternatives like Foo { ... }
or Foo { ..some_keyword }
to the "Rationale and alternatives" section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that is already the case?
x if x == Foo { a, b }
matches less than Foo { a, b }
assuming a
and b
variables are in scope.
Although f you define a
and b
as constants then Foo { a, b }
will match the exact value, which is.... interesting.
I don't think this is a problem, it is expected that patterns behave differently from expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In particular, the syntax
Foo { .. }
mirrors the identical and already
existing pattern syntax.
Indeed, and that's a downside -- it mirrors the syntax but has different semantics. The text here makes it sound like that's a good thing; I think that should be reworded (and not just called out under "Downsides").
text/0000-default-field-values.md
Outdated
} | ||
``` | ||
|
||
```rust |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth also comparing against derive macros like typed-builder, which at least in my experience are a very common solution to this problem.
text/0000-default-field-values.md
Outdated
facilitates specification of field defaults; or it can directly use the default | ||
values provided in the type definition. | ||
|
||
### `structopt` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe replace with clap? since clap v3, basically all structopt stuff was integrated into clap and structopt is de-facto deprecated. https://docs.rs/structopt/0.3.26/structopt/index.html#maintenance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is a holdover from the original RFC written a number of years ago when structopt
was in common usage.
text/0000-default-field-values.md
Outdated
``` | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see a section on differences from FRU (especially the interaction with privacy and #[non_exhaustive]
) including why it is the way it is and the reason for diverging from it in this RFC. And possibly a future possibility about how they can be made more consistent in the future.
I'm generally a big fan of this RFC, but undoing past mistakes by adding more features without fixing the features we have leads to an uneven and complex language surface. So let's try to make sure we can do both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it is reasonable to change this RFC to explicitly follow RFC-0736, and disallow the construction of types with private fields, if that will make this RFC less controversial. Particularly when considering the interaction with #[derive(Default)]
, the feature is useful on its own without expanding the field privacy rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not saying it's wrong, but I want to understand and consider the rationale for RFC-0736. I dislike it, but think there was probably a good reason for it, even if I can't remember what it was at the moment. That reason may or may not apply here.
edit: I would be happy to help with this btw, it's just too late for me to do right now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with FRU is all about it not having the desugaring that lots of people expect.
There would be no problem with FRU and privacy if it only allowed you to modify public fields on a type, but the problem is that what it does is secretly duplicate private fields on a type, which is blatantly wrong for literally-but-not-semantically Copy
types like the pointer in a Vec
. https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/struct.20update.20syntax.20with.20non.20exhaustive.20fields/near/438944351
I think the way forward here is to fix FRU (sketch in https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/struct.20update.20syntax.20with.20non.20exhaustive.20fields/near/438946306) so that it can obviously work fine with non_exhaustive
(and private fields), rather than try to work in an exception here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sketch uses a completely new syntax, so we wouldn't be fixing FRU so much as deprecating it and replacing it with something else.
It seems better to design this one to work the way we want from the beginning? Especially since people will have to opt in and might rely on restrictions we implement today (even if we document that those restrictions will be lifted in the future.. people don't usually read the docs on language features so much as try them out in the playground).
@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 (I also tend to feel that |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
@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 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:
That makes sense to me as a story and as a plan. |
This comment was marked as off-topic.
This comment was marked as off-topic.
text/0000-default-field-values.md
Outdated
As you saw in (7), `Vec::new()`, a function call, was used. | ||
However, this assumes that `Vec::new` is a *`const fn`*. That is, when you | ||
provide a default value `field: Type = value`, the given `value` must be a | ||
*constant expression* such that it is valid in a [`const` context]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the other requirements on the value type? Vec
example indicates the type doesn't need to be Copy
. Does it need to be Clone
? The decision here might be especially important for ZSTs which are used as some kind of token and are supposed to guarantee that they cannot be conjured/cloned out of thin air
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the other requirements on the value type?
Vec
example indicates the type doesn't need to beCopy
. Does it need to beClone
?
I don't think there should be. The value is given as a const expression, which can be thought as being constructed on each usage. This is also why [CONSTANT; 10]
always work and does not require the element type to be Clone
or Copy
.
The decision here might be especially important for ZSTs which are used as some kind of token and are supposed to guarantee that they cannot be conjured/cloned out of thin air
If the definition site explicitly mark a default field by using a valid way to construct it. It does not create any extra way to conjure a ZST. It's the same as they define a impl S { pub const DEFAULT_FIELD: ArbitraryNoCopyTy = some_constructor(); }
and users write S { field: S::DEFAULT_FIELD }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the other requirements on the value type?
I would say there are no requirements on the type. The requirements are just on the expression used to generate it, which are the same as the requirements needed in a const { … }
.
This comment was marked as off-topic.
This comment was marked as off-topic.
Initial implementation of `#[feature(default_field_values]`, proposed in rust-lang/rfcs#3681. Support default fields in enum struct variant Allow default values in an enum struct variant definition: ```rust pub enum Bar { Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Allow using `..` without a base on an enum struct variant ```rust Bar::Foo { .. } ``` `#[derive(Default)]` doesn't account for these as it is still gating `#[default]` only being allowed on unit variants. Support `#[derive(Default)]` on enum struct variants with all defaulted fields ```rust pub enum Bar { #[default] Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Check for missing fields in typeck instead of mir_build. Expand test with `const` param case (needs `generic_const_exprs` enabled). Properly instantiate MIR const The following works: ```rust struct S<A> { a: Vec<A> = Vec::new(), } S::<i32> { .. } ```
Initial implementation of `#[feature(default_field_values]`, proposed in rust-lang/rfcs#3681. Support default fields in enum struct variant Allow default values in an enum struct variant definition: ```rust pub enum Bar { Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Allow using `..` without a base on an enum struct variant ```rust Bar::Foo { .. } ``` `#[derive(Default)]` doesn't account for these as it is still gating `#[default]` only being allowed on unit variants. Support `#[derive(Default)]` on enum struct variants with all defaulted fields ```rust pub enum Bar { #[default] Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Check for missing fields in typeck instead of mir_build. Expand test with `const` param case (needs `generic_const_exprs` enabled). Properly instantiate MIR const The following works: ```rust struct S<A> { a: Vec<A> = Vec::new(), } S::<i32> { .. } ```
Initial implementation of `#[feature(default_field_values]`, proposed in rust-lang/rfcs#3681. Support default fields in enum struct variant Allow default values in an enum struct variant definition: ```rust pub enum Bar { Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Allow using `..` without a base on an enum struct variant ```rust Bar::Foo { .. } ``` `#[derive(Default)]` doesn't account for these as it is still gating `#[default]` only being allowed on unit variants. Support `#[derive(Default)]` on enum struct variants with all defaulted fields ```rust pub enum Bar { #[default] Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Check for missing fields in typeck instead of mir_build. Expand test with `const` param case (needs `generic_const_exprs` enabled). Properly instantiate MIR const The following works: ```rust struct S<A> { a: Vec<A> = Vec::new(), } S::<i32> { .. } ```
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? |
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 |
This moves the syntax we chose to align closer with actual Rust code. See rust-lang/rfcs#3681 for more details.
This moves the syntax we chose to align closer with actual Rust code. See rust-lang/rfcs#3681 for more details.
Co-authored-by: Felix S Klock II <pnkfelix@pnkfx.org>
Initial implementation of `#[feature(default_field_values]`, proposed in rust-lang/rfcs#3681. Support default fields in enum struct variant Allow default values in an enum struct variant definition: ```rust pub enum Bar { Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Allow using `..` without a base on an enum struct variant ```rust Bar::Foo { .. } ``` `#[derive(Default)]` doesn't account for these as it is still gating `#[default]` only being allowed on unit variants. Support `#[derive(Default)]` on enum struct variants with all defaulted fields ```rust pub enum Bar { #[default] Foo { bar: S = S, baz: i32 = 42 + 3, } } ``` Check for missing fields in typeck instead of mir_build. Expand test with `const` param case (needs `generic_const_exprs` enabled). Properly instantiate MIR const The following works: ```rust struct S<A> { a: Vec<A> = Vec::new(), } S::<i32> { .. } ```
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. |
2235794
to
80e8d0b
Compare
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 andthereby allowing those to be omitted from initializers. When deriving
Default
,the provided values will then be used. For example:
Rendered
Tracking: