Skip to content
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

Merged
merged 37 commits into from
Nov 3, 2024
Merged

Conversation

estebank
Copy link
Contributor

@estebank estebank commented Aug 22, 2024

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:

@scottmcm scottmcm added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 22, 2024
}
```

These can then be used in the following way with the Functional Record Update syntax, with no value:
Copy link
Member

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)...

Copy link
Member

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.

Copy link
Member

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 Show resolved Hide resolved
@estebank estebank changed the title default-field-values: initial version RFC: Default field values Aug 23, 2024
@estebank estebank changed the title RFC: Default field values Default field values Aug 23, 2024
@estebank estebank changed the title Default field values [RFC] Default field values Aug 23, 2024
Comment on lines 588 to 593
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, .. };
```
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

@clarfonthey clarfonthey Aug 23, 2024

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.

Copy link
Member

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(),
}

Copy link
Member

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) Defaults move to being deriveable -- 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.

Copy link
Contributor Author

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.

Copy link

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.

Comment on lines 815 to 817
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.
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@max-niederman max-niederman Aug 23, 2024

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.

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.

Copy link
Member

@RalfJung RalfJung Sep 3, 2024

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 Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
}
```

```rust
Copy link
Contributor

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 Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
facilitates specification of field defaults; or it can directly use the default
values provided in the type definition.

### `structopt`
Copy link
Member

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

Copy link
Member

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 Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
text/0000-default-field-values.md Outdated Show resolved Hide resolved
```

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@tmandry tmandry Aug 24, 2024

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 :)

Copy link
Member

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.

Copy link
Member

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).

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Oct 22, 2024

@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 rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Oct 22, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Oct 22, 2024

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

@traviscross
Copy link
Contributor

traviscross commented Oct 22, 2024

@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

This comment was marked as off-topic.

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].

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

Copy link

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?

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 }.

Copy link
Member

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 { … }.

@rodrimati1992

This comment was marked as off-topic.

estebank added a commit to estebank/rust that referenced this pull request Oct 25, 2024
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> { .. }
```
estebank added a commit to estebank/rust that referenced this pull request Oct 25, 2024
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> { .. }
```
estebank added a commit to estebank/rust that referenced this pull request Oct 26, 2024
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> { .. }
```
@Ciel-MC
Copy link

Ciel-MC commented Oct 26, 2024

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
Copy link

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

hughsie added a commit to fwupd/fwupd that referenced this pull request Oct 26, 2024
This moves the syntax we chose to align closer with actual Rust code.

See rust-lang/rfcs#3681 for more details.
superm1 pushed a commit to fwupd/fwupd that referenced this pull request Oct 28, 2024
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>
estebank added a commit to estebank/rust that referenced this pull request Oct 29, 2024
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> { .. }
```
@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. to-announce and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Nov 1, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Nov 1, 2024

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 traviscross merged commit abf3bf8 into rust-lang:master Nov 3, 2024
@traviscross
Copy link
Contributor

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.