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

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open

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

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

@lolbinarycat
Copy link
Contributor

I wasn't trying to solve that, so in my proposal a: i32 = 17 would take precedence over impl Default on the same struct when S { .. } is used. The Default impl would only be used for fields that have no explicit default.

what happens in this case:

struct Weird {
  a: u8 = 1,
  b: u8,
}

impl Default for Weird {
  fn default() -> Self {
    Self {
      a: 2,
      b: 3,
    }
  }
}

what does dbg!(Weird{ .. }) print? does it use the u8 Default or the Weird one?

@skogseth
Copy link

skogseth commented Sep 28, 2024

I wasn't trying to solve that, so in my proposal a: i32 = 17 would take precedence over impl Default on the same struct when S { .. } is used. The Default impl would only be used for fields that have no explicit default.

what happens in this case:

struct Weird {
  a: u8 = 1,
  b: u8,
}

impl Default for Weird {
  fn default() -> Self {
    Self {
      a: 2,
      b: 3,
    }
  }
}

what does dbg!(Weird{ .. }) print? does it use the u8 Default or the Weird one?

This would give a compilation error saying that you need to specify the value for b, as it doesn't have a default field value specified. Nevermind, I think I misunderstood what you were responding to.

@estebank
Copy link
Contributor Author

what does dbg!(Weird{ .. }) print? does it use the u8 Default or the Weird one?

@lolbinarycat under the proposal that you're replying to, it would be using the impl Default for u8.

I do not think that allowing Weird { .. } should be part of this RFC, at most an opt-in future change.

@PoignardAzur
Copy link

PoignardAzur commented Sep 29, 2024

Something I haven't seen addressed: does the following code work?

#[derive(Default)]
struct Foobar<T> {
    foo: [T; 10] = Default::default(),
    bar: Option<T> = Default::default(),
}

Because if so, that paragraph from the RFC doesn't really hold:

One thing to notice, is that taking default values into consideration during the desugaring of #[derive(Default)] would allow to side-step the issue of our lack of perfect derives, by making the desugaring syntactically check which type parameters correspond to fields that don't have a default field, as in the expansion they will use the default value instead of Default::default().

@traviscross
Copy link
Contributor

traviscross commented Sep 30, 2024

Perhaps we're trying to fill out a table that looks something like this:

On ADT: no Default impl #[derive(Default)] impl Default for
= val on all fields
= val on some fields1
= val on some fields2
= val on some fields3
= val on some fields4
= val on some fields5
= val on no fields
#[required] on some fields6

In each square are the answer to questions such as:

  • Does this combination make sense at all?
  • Should this square have the same behavior as another square?
  • Should we be linting for or against anything here?
  • Should { .. } be allowed and what should it mean?
  • If Default is implemented, how do the = val fields affect its behavior?
  • Should { .. } and {..<_>::default() } have the same behavior, or both work or not work in this combination?
  • If something doesn't work (e.g. { .. } syntax), are we guaranteeing that it never will work (e.g. because users can rely on this to express that a field is "mandatory"), or might we make it work later?
  • Etc.

Footnotes

  1. All fields without = val are of a type that itself can be constructed with { .. }.

  2. All fields without = val are of a type that itself can be constructed with <_ as const Default>::default()6.

  3. All fields without = val are of a type that itself can be constructed with <_ as Default>::default().

  4. All fields without = val are of a type that itself can be constructed with all or some non-trivial subset of the above.

  5. All fields without = val are of a type that itself does not have any kind of default.

  6. If that were a thing. 2

@estebank
Copy link
Contributor Author

@PoignardAzur because of the way the expansion would look, you would get the following error on the struct definition:

error[E0277]: the trait bound `T: Default` is not satisfied
 --> src/lib.rs:9:18
  |
9 |             foo: Default::default(),
  |                  ^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `T`, which is required by `[T; 10]: Default`
  |
  = note: required for `[T; 10]` to implement `Default`
help: consider restricting type parameter `T`
  |
6 | impl<T: std::default::Default> Default for Foobar<T> {
  |       +++++++++++++++++++++++

forcing you to write

#[derive(Default)]
struct Foobar<T: Default> {
    foo: [T; 10] = Default::default(),
    bar: Option<T> = Default::default(),
}

This is predicated, of course, on const Default becoming a thing, but it would still apply if you had a const fn option_default<T>() -> Option<T> { .. }.

@PoignardAzur
Copy link

PoignardAzur commented Sep 30, 2024

Actually, thinking about it, you would probably get an error even if Default isn't derived, right? Otherwise Foobar { .. } would resolve to an invalid expression if T doesn't implement Default.

Which means every default value must be valid with the type's default bounds, which means a field with a default value cannot possibly introduce new Default bounds, which means ignoring it when generating where clauses in #[derive(Default)] is fine.

@tmandry
Copy link
Member

tmandry commented Sep 30, 2024

why not just forbid manually implementing Default on structs with field defaults? that gets around the nasty ..Default::default() parity issue, and is a restriction that can be relaxed in the future.

Erm, yes, obviously we should do that @lolbinarycat :). I like it.

Regarding the outstanding analysis that, @tmandry, you did here on the interaction of this with non_exhaustive and field privacy... I might prefer to see us at least try to solve this along with the rest of the design and while we have this loaded into cache. (Landing this in nightly under an experimental feature flag might help here too.)

I tend to agree that we should settle on Option 2 and probably Option 3, and that these can be stabilized one at a time and after the rest of the RFC, if desired. Do you have reservations about this @estebank? I am mindful of scope creep but my feeling is that the interaction is quite important and within the scope of the RFC.

The remaining question is what to call the opt-in, which is less important than settling on the semantics we're going for. If the lang team meets about this RFC we can accelerate the bikeshed painting, or we can leave it as an open question in the RFC.

@scottmcm
Copy link
Member

scottmcm commented Oct 1, 2024

On Foo { .. } vs Foo::default():

I think it's fine if they're not forced to be the same so long as the easy ways to do things are all consistent. For example, so long as it's easier than getting Hash and PartialEq consistent, I think it's fine. (We can even add a "MUST" to the Default trait's docs about that; it just won't be an unsafe promise. Just like PartialEq and PartialOrd consistency.)

So realistically, what does it need to get things inconsistent? It needs all of

  1. A field with a default in the type
  2. A manually-implemented Default
  3. Specifying the field in the initializer unnecessarily

I don't think that banning (2) is acceptable (cc @tmandry because of their post above), because we don't have perfect derive. I really don't want to have the outcome here be that you're not allowed to write something like the following:

struct Foo<T> { a: u32 = 4, b: Option<T> = None }
impl<T> Default for Foo<T> { fn default() -> Self { Self { .. } }

because that seems entirely reasonable and useful.

So what if we target (3) instead? A lint for "hey, in the Default::default implementation you're specifying a field that has a default in the struct definition (and which doesn't match) -- you probably don't want to do that".

I don't think we need to ban Weird as a hard error. A pit of success is more than enough.

(With the lint described, this is already substantially easier to get right than PartialEq/PartialOrd, as seen from threads like https://users.rust-lang.org/t/panic-on-sort-of-f32-s-with-f32-total-cmp-in-1-81-0/117675?u=scottmcm.)

@traviscross
Copy link
Contributor

traviscross commented Oct 1, 2024

I don't think that banning (2) is acceptable... because we don't have perfect derive. I really don't want to have the outcome here be that you're not allowed to write something like the following:

struct Foo<T> { a: u32 = 4, b: Option<T> = None }
impl<T> Default for Foo<T> { fn default() -> Self { Self { .. } }

because that seems entirely reasonable and useful.

Maybe that's what #[derive(Default)] should mean when all fields have syntactic defaults? The RFC suggests as much in the motivation here:

One thing to notice, is that taking default values into consideration during the desugaring of #[derive(Default)] would allow to side-step the issue of our lack of [perfect derives], by making the desugaring syntactically check which type parameters correspond to fields that don't have a default field, as in the expansion they will use the default value instead of Default::default(). By doing this a user can side-step the introduction of unnecessary bounds by specifying a default value of the same return value of Default::default().

It specifically suggests the expansion of...

#[derive(Default)]
struct Foo<T> {
    bar: Option<T> = None,
}

...to:

struct Foo<T> {
    bar: Option<T>,
}
impl<T> Default for Foo<T> {
    fn default() -> Foo<T> {
        Foo {
            bar: None,
        }
    }
}

(That would of course have some SemVer-relevant effects worth considering.)

@scottmcm
Copy link
Member

scottmcm commented Oct 1, 2024

by making the desugaring syntactically check which type parameters correspond to fields that don't have a default field

It's really non-obvious to me whether that's even possible. At the very least it's got to be quite nuanced in cases where there are multiple type parameters with potential trait bounds between them, such that you can still get to a type parameter without ever actually mentioning it in that type or that initialization expression.

And it's hard to say whether a _phantom: PhantomData<T> = PhantomData ought to suppress T: Default. Being inconsistent with every other kind of derive might even be worse than being smarter, if it ends up resulting in extra confusion.

@PoignardAzur
Copy link

And it's hard to say whether a _phantom: PhantomData<T> = PhantomData ought to suppress T: Default. Being inconsistent with every other kind of derive might even be worse than being smarter, if it ends up resulting in extra confusion.

The solution for that could be to add opt-out annotations to other traits as well.

You could add a #[marker] annotation to fields, that would let macros know that the field is a ZST. So PartialEq, Eq, PartialOrd, Ord, Hash, and Debug would know to ignore the field. Possibly Clone and Copy too, depending on the semantics of that annotation.

@PoignardAzur
Copy link

It's really non-obvious to me whether that's even possible. At the very least it's got to be quite nuanced in cases where there are multiple type parameters with potential trait bounds between them, such that you can still get to a type parameter without ever actually mentioning it in that type or that initialization expression.

This is probably something you should built team consensus on, because it impacts multiple RFCs. For example, we were talking about having a macro generate your where bounds in the context of #3698.

It would be nice to have a canonical "If your type has these type parameters and these fields, it should generate these bounds" guideline.

@scottmcm
Copy link
Member

scottmcm commented Oct 2, 2024

@PoignardAzur The two things that work well are the "bound all the type parameters" for semver that all the built-in ones use, and the "bound all the field types" approach of so-called perfect derive.

Trying to use tokens to figure out trait usage will always be sketchy at best.

@tmandry
Copy link
Member

tmandry commented Oct 3, 2024

I was assuming we could make #[derive(Default)] work by suppressing bounds for fields that specify a default, like the RFC says. @scottmcm raises a few issues that are compelling enough to convince me that this isn't trivial (I also dug up this post on the semver hazards associated with perfect derive), and it probably isn't a can of worms we want to chew on in this RFC.

If we can't do that and make manual derives an error with ~no cost, I agree we should use a lint and tailor it as much as possible to make sure no one ever creates divergence accidentally. (I like Scott's suggestion to lint on already-defaulted fields set in the Default impl.)

With that kind of checking it won't be much different from the fact that you can do side-effecting things in a Deref or PartialOrd impl or write bugs there.. you can, but it's obvious that you shouldn't, and in practice no one will other than for weird party tricks.

@tmccombs
Copy link

tmccombs commented Oct 3, 2024

What about the case all fields have a default value, would it be reasonable to allow deriving Default in that case, even if we don't allow it if only some of the fields have defaults.

@SOF3
Copy link

SOF3 commented Oct 3, 2024

@tmccombs do you mean allowing #[derive(Default)] if fields.all(|field| field.type.implements(Default) || field.default_value.is_some())? I don't understand how that's different from the above

@PoignardAzur
Copy link

I'd like to fight back a bit on the "inferring bounds from tokens is intractable" claim, because I do think it's worth considering.

You mentioned associated types in another post. Here's an example of a "tricky" struct using them:

#[derive(Default)]
struct MyStruct<T, U>
where
    T: SomeTrait<U>,
{
    t: T,
    u: T::Assoc,
}

trait SomeTrait<T> {
    type Assoc;
}

impl<T, U> SomeTrait<U> for T {
    type Assoc = U;
}

On the one hand, yes, the U type is "smuggled" in without appearing as a token in the fields. On the other hand, here's what the #[derive(Default)] macro currently expands to:

impl<T: ::core::default::Default, U: ::core::default::Default> ::core::default::Default
    for MyStruct<T, U>
where
    T: SomeTrait<U>,
    T::Assoc: ::core::default::Default,
{
    #[inline]
    fn default() -> MyStruct<T, U> {
        MyStruct {
            t: ::core::default::Default::default(),
            u: ::core::default::Default::default(),
        }
    }
}

The macro already needs to bind on T::Assoc: Default. There's already some non-trivial token parsing going on; this parsing is already somewhat unprincipled (it will generate as bound for T::Assoc, but not for <T as MyTrait>::Assoc), and you can already come up with ways to defeat it today with TAITs and the like.

So the question isn't "Should derive macros generate one bound per argument or should they consider tokens in fields", because they already do the latter. Rather, it's "How complex should the token parsing be?".

Now, granted, existing derive macros do this parsing on top of generating one bound per argument, and I'm suggesting replacing the one-bound-per-argument rule entirely. That does add potential for brittleness.

But I'd argue the difference isn't quite as cut and dry as you think. It's worth examining the possibility further.


Note that this is also relevant to #3683. If you write:

#[derive(Default)]
enum MyEnum<T, U> {
    #[default]
    Foo {
        field: Option<T>,
    }
    Bar(U),
}

You'd like your derive to assume T: Default, not T: Default, U: Default.

@tmccombs
Copy link

tmccombs commented Oct 3, 2024

@SOF3 no I mean deriving Default would be allowed if:

fields.all(|field| field.default_value.is_some()) || (fields.all(|field| field.type.implements(Default)) && fields.all(|field| field.default_value.is_none()))

So if all fields have defaults, the Default impl would use all the default values. If no fields have default values, the implementation of Default would use the implementation of Default for all the fields.

@scottmcm
Copy link
Member

scottmcm commented Oct 3, 2024

You mentioned associated types in another post. Here's an example of a "tricky" struct using them:

That's just doing both the bound-the-parameter and the bound-the-field cases I mentioned. Bound the whole field certainly works, though I'm surprised it bothers doing that. I guess it wanted more "well it just doesn't exist" cases and fewer "it fails to compile" cases. (I find that odd, because a field of type Foo<T> might be just a much not-default as a field of type <T as Blah>::Bar, so I don't see why it'd bound one but not the other.)

Note that this is also relevant to #3683. If you write: […]
You'd like your derive to assume T: Default, not T: Default, U: Default.

Wouldn't you want Option<T>: Default?

@tmandry
Copy link
Member

tmandry commented Oct 3, 2024

@PoignardAzur

I agree we can probably come up with a rule that works based on parsing the struct. The real concern is that it can introduce semver hazards to be too clever about when a derive is implemented.

The expansion you posted is interesting; it demonstrates that we already do this to some extent. That lends some credibility to the idea that it will "probably be fine" to do this in practice, but it still makes me nervous. I would rather not increasingly require library maintainers to rely on tooling to catch subtle instances of semver breakage like this.

As a sketch, I think if we had some combination of

#[derive(Default where T: Default)]

for adding bounds, and a semver_hazards lint that fires when your derives miss bounds in a way that creates hazards, we could get away with always doing perfect derive (probably over an edition). But that's a lot of work that needs actual semantic analysis and pretty out of scope for this RFC.

@crumblingstatue
Copy link

crumblingstatue commented Oct 3, 2024

My personal problem with assigning default values at struct-definition site is that it conflates data with behavior.

There is no reason why a struct couldn't have multiple sets of defaults.
An example would be something like egui::Visuals, which has "dark" and "light" defaults.

In my opinion, when exploring alternatives to this RFC, we should strongly consider ones that detach the concept of a "set of defaults" from the struct definition itself. In my opinion the place for defining a set of defaults should be in an impl block.

For those who would bring up #[derive(Default)] on structs (and helper proc macro crates for smart-derives that help define values), I would like to remind you that it's just syntax sugar for implementing a trait.

Functions already provide a decent way to define a set of defaults, and they can be used with the struct update syntax (..default()), as acknowledged by the RFC.

One of the main problems is not being able to "delegate" the defaults for fields you don't want to explicitly define.
This is most often because the types of the fields themselves have sensible defaults, e.g., they implement the Default trait.
But there is currently no way to "delegate" the default values to the Default impl of each field.

impl MyStruct {
    const fn my_defaults() -> Self {
        Self {
            field1: Default::default(),
            field2: Default::default(),
            field3: Default::default(),
            field4: Default::default(),
            field5: Default::default(),
            field6: Default::default(),
            field7: Default::default(),
            field8: Default::default(),
            // I only care about explicitly defining this
            field9: 42,
            field10: Default::default(),
            field11: Default::default(),
            field12: Default::default(),
            // And this
            field13: "Explicitly set",
            field14: Default::default(),
            field15: Default::default(),
            field16: Default::default(),
            field17: Default::default(),
            field18: Default::default(),
            field19: Default::default(),   
        }
    }
}

If there was a way to delegate fields you're not interested in explicitly setting to a trait method, that would solve a large chunk of the motivation of this RFC.

impl MyStruct {
    const fn my_defaults() -> Self {
        Self {
            // I only care about explicitly defining this
            field9: 42,
            // And this
            field13: "Explicitly set",
            // Pseudo syntax for delegating defaults
            for .. use Default::default
        }
    }
}

I understand that part of the motivation of this RFC is usability in const contexts, which would make my proposal above depend on const trait impls (Like being able to constly impl Default::default), but in my opinion it would be worth waiting for this feature.

There are other possible alternatives for defining default-sets for a type, without having to tie the defaults to the definition of said type. I strongly suggest exploring such alternatives.

@tmccombs
Copy link

tmccombs commented Oct 4, 2024

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

SOF3 commented Oct 4, 2024

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

skogseth commented Oct 5, 2024

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

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

crumblingstatue commented Oct 5, 2024

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

crumblingstatue commented Oct 5, 2024

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

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. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.