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

Default Struct Field Values #1806

Closed
wants to merge 7 commits into from
Closed

Default Struct Field Values #1806

wants to merge 7 commits into from

Conversation

KodrAus
Copy link
Contributor

@KodrAus KodrAus commented Dec 2, 2016

@KodrAus KodrAus changed the title Default Fields Default Struct Field Values Dec 3, 2016
@killercup
Copy link
Member

killercup commented Dec 3, 2016

Interesting, I like it. Not something that really bothered me so far, but I can see the appeal.

You mention "constant expressions" a few times. Does mean you want to allow const fn as well? How much complexity do you want to allow in the default value? I think a pretty common use case might be to initialize some heap-allocated data structure like Vec or HashMap, which AFAIK is not possible in a constant expression.

For alternatives, I see there is derive-new crate (using macros 1.1 which is currently unstable), that has an open issue for adding default values with attributes (I assume something like adding #[default(42)] to an i32 field).

@leoyvens
Copy link

leoyvens commented Dec 3, 2016

@killercup const fn is included. To allow an empty Vec we would need the alternative of allowing Default::default. It is not theoretically impossible for Vec::new() to be const fn since it dosen't actually allocate anything, but that is probably far from being possible in practice.

@eddyb
Copy link
Member

eddyb commented Dec 3, 2016

@leodasvacas We could make Vec::new const fn right now AFAIK, if we wanted to. But a const fn <Vec as Default>::default is trickier to design (you don't want a separate const fn default).

@leoyvens
Copy link

leoyvens commented Dec 3, 2016

@eddyb Ah, I missed #1440. Awesome!

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 3, 2016

The goal is to maintain that expectation of cheapness for fields that can be initialised without you knowing. We were talking about allowing Default::default too, because you expect that to be cheap, but what's proposed is the conservative option.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 5, 2016
@nrc nrc self-assigned this Dec 5, 2016
@jFransham
Copy link

I have a problem with this not playing nice with the trait machinery, if you have a function that takes a generic A: Default it won't operate on a struct that has default values for all the fields, despite that meaning the same thing. It means that there are two ways to do the same thing that don't interact. I can understand that

#[derive(Default)]
struct Foo {
    a: i32,
    b: i32,
}

Foo {
    a: 10,
    ..Default::default(),
}

is slightly unergonomic, but it's explicit and allows a user wondering where values come from to follow the Default::default. If you want to only have defaults for some values, you can use the builder pattern. Anonymous structs/named arguments would fix that too, since you could have a function that looks like

initialize_b_only({
    a: 10
})

(syntax is imaginary).

@dtolnay

This comment has been minimized.

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 5, 2016

@jFransham I think this plays just as nicely with traits as Clone does. You'd have the same problem with A: Clone unless you derive Clone, even though all fields may be Clone.

Maybe this is a different case because you're actively giving each field a default value and the result is that it can be initialised with no outside data. So it basically is Default. The question is whether the compiler should automatically take the step of deriving it for you if you don't.

I think an important distinction between what we've got now and what this RFC proposes is that it lets structs define their own default for types on a per-field basis. So instead of having one value for Default that may not be suitable for your needs (what if I don't want my i32 to default to 0?) you can make the default value of a field the responsibility of that field instead of the external type.

Having less boilerplate around builders would definitely make them more useful here, but are still more effort.

@KodrAus

This comment has been minimized.

}
```

We can add a new field `b` to this `struct` with a default value, and the calling code doesn't change:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless .. is made optional in patterns new field b can't be added here because it would break patterns Foo { a, b }.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if structure has a special private field to force .. in patterns - Foo { a, b, .. /*__hidden: ()*/ }, then it can be extended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My immediate thought is that .. in patterns would also have to be optional then for consistency. I'm cautious about that though because it might increase the scope of this thing too much.

The other option is make .. required for structs with defaults, or stop pretending it's always backwards compatible.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unfortunate for partial construction and partial destructoring to have an inconsistent syntax. If making .. optional in patterns is an acceptable way forward, then I would suggest for this RFC to mention that because of patterns you need a private field for backwards compatibility, and leave changing pattern syntax to a future RFC.


## Explicit syntax for opting into field defaults

Field defaults could require callers to use an opt-in syntax like `..`. This would make it clearer to callers that additional code could be run on struct initialisation, weakening arguments against more powerful default expressions. However it would prevent field default from being used to maintain backwards compatibility, and reduce overall ergonomics. If it's unclear whether or not a particular caller will use a default field value then its addition can't be treated as a non-breaking change.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However it would prevent field default from being used to maintain backwards compatibility

There's no backward compatibility already (https://github.com/rust-lang/rfcs/pull/1806/files#r90957633), so mirroring pattern syntax or omitting .. is mostly a stylistic choice (unless, again, .. is made optional in patterns).

@dtolnay

This comment has been minimized.

@withoutboats
Copy link
Contributor

withoutboats commented Dec 6, 2016

Not sure how I feel about this RFC, but syntactically I would strongly prefer some indication of the missing fields in the constructor, possibly just .. with no expression.

That is:

struct Foo {
    bar: u32 = 0,
    baz: u32 = 1,
}

let foo = Foo { bar: 7, .. };
let foo = Foo { .. };

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 7, 2016

@withoutboats I'm not against requiring .. to invoke field defaults. It'd work pretty much the same as FRU does now so you could add a private field with a default value to force callers to use the ... But I don't think it's worth peppering things with syntax if that's not solving a real problem. What kinds of problems do you see this explicitness solving?

@Mark-Simulacrum
Copy link
Member

I support adding .. to initialization expressions that want to use default field values, since it makes it clear that there are more fields in the struct being instantiated. When reading code (especially rustc code, where many structs have 10+ fields) I want to know that either the initialization expression I'm seeing is complete or that I can look for X to find the other fields.

Right now, X is usually an implementation of Default, or another struct; both of which are easy to find. Omitting the .. makes it more likely that someone reading the code wouldn't know if they've found the correct struct or not (especially with the re-exports prevalent in larger codebases which make tracing imports difficult).

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 7, 2016

Well that would also save having to change the destructuring pattern so ok I'm sold.

@withoutboats
Copy link
Contributor

Yea, the goal would be to make it clear to people what is happening in the code they're reading, since they aren't necessarily familiar with the definition of the structs involved. I think this is a case where "explicit is better than implicit" is a helpful maxim.

@Mark-Simulacrum
Copy link
Member

One thing that I haven't yet seen brought up is the interaction with (potential, admittedly) function argument defaults, along with defaults in other places. Do we want to settle on = as the syntax for those? I think it's good; but if there's any problems with that, we should discuss them now.

@withoutboats
Copy link
Contributor

@Mark-Simulacrum EXPR = EXPR is unfortunately a valid expression which evaluates to ().

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 7, 2016

Now something to figure out is how to explain field defaults with .. syntax vs FRU to new users. From a caller point of view I think it could be pretty unintuitive knowing which feature they need to use and why.

@keeperofdakeys
Copy link

Requiring .. is definitely a good thing, it fits in with the more explicit nature of rust. It also improves semantics with future changes. For example, if a new field is added with a default, only code that opts-in with .. will receive it - the rest will get compile errors.

@crumblingstatue
Copy link

crumblingstatue commented Dec 7, 2016

The problem with a constructor is that you need one for each combination of fields a caller can supply.

I would rather like to see work on named and optional arguments, which would allow constructors to solve this problem, and make other APIs more ergonomic as well.

If Rust had to pick between optional fields for structs, and optional/named function arguments, I would pick the latter, as it allows more ergonomic APIs, e.g. Window::new(width: 200, height: 200) vs. Window::new(WindowArgs{ width: 200, height: 200 }).

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 7, 2016

@crumblingstatue I don't see why Rust couldn't have both default struct fields and default function arguments.

@leoyvens
Copy link

leoyvens commented Dec 7, 2016

As @KodrAus already mentioned, making this explicit with .. draws a subtlely false parallel with FRU, because this can be used to default private fields and FRU can't. The ugly part is: explicit syntax keeps leaking the presence of private fields to the public API. Adding a private field to a struct that has only public fields remains a breaking change. This is already a problem because of patterns, but we can solve this by changing pattern syntax. Explicit syntax would make this a pervasive and permanent problem, by design, so it seems like a non-starter to me.

@crumblingstatue This is a much smaller adition to the language than named or optional parameters and yet solves some use cases that would need both of those features, so it's a win for those who want such features even if it dosen't solve all use cases.

Edit: Change of wording.

@KodrAus
Copy link
Contributor Author

KodrAus commented Dec 7, 2016

@leodasvacas I'm thinking a pragmatic way forward would be to propose this with an explicit syntax so it's in line with the current state of the world. The biggest benefit of implicit syntax kind of falls over with patterns right now, so they seem like a coupled issue. If callers want expliciteness it's going to come at the cost of incompatibility because everything is surfaced to them. A future RFC could propose allowing implicit .. for field defaults and patterns together. Unless that's not the best aproach. I'm open to suggestions.

The similarity but difference to FRU is the problem I'm thinking about at the moment.

@burdges burdges mentioned this pull request Nov 3, 2018
@H2CO3
Copy link

H2CO3 commented Nov 4, 2018

I oppose this RFC, for the following reasons:

  1. When deriving Default, supplied field defaults are used instead of the type default. This is a feature of #[derive(Default)].

    I'm not a fan of this. It makes sense, but how much more special will the Default trait be as a consequence of it? Parallel to recent discussions about From/Into, the beauty of these traits was that they didn't require language or compiler support so far. (I know that #[derive(Default)] is in the compiler, but theoretically it could be in a library instead – it's not very highly coupled with the language, and it doesn't need to be.)

  2. Symmetry with Destructuring

    Field defaults allow structs to be non-exhaustively constructed and deconstructed using the same syntax:

    Why is that a symmetry? Conversely, they are doing two completely different things with the same syntax, not two aspects or "directions" of the same thing. Destructuring a struct and throwing away fields doesn't need extra information, there's nothing magical going on there, you just don't use certain fields.

    Construction with default values, however, hides the fact that there is extra information required to instantiate a struct without specifying all of its fields. .. suggests that there's something "unimportant" or "ignorable" going on, whereas that is pretty much not the case – the user is supplying values without knowing about it. In contrast, .. Default::default() makes you know that you are supplying the values, while still keeping it simple (and syntactically lightweight).

  3. The RFC also lists the ability to construct structs with private fields using the struct literal syntax as an advantage. I disagree – to me, that's a straight-up disadvantage. The reason is this. Most if not all structs with private fields have some sort of internal invariant, state, or other delicate property which must be maintained by their public API.

    Changing the internals of how these types are supposed to work might sometimes be impossible without enforcing certain initial conditions on construction. For example, one might want to perform some checks in the constructor and make a decision base don them, or use values not local to the call site (that can't be, as such, specified once in the struct definition, for all future instantiations, much like there exist expressions which just couldn't be supplied as default function arguments).

    This in turn means that a struct with private fields can transition from being able to be initialized using a literal, to the state of requiring a constructor function (e.g. T::new() and/or a manualDefault::default impl). This means that previously non-breaking private changes (ie. changes in the implementation of an already-existing constructor function) now leak to the public interface and cause a breaking change. This seems highly undesirable.

Overall, this RFC, while seems somewhat useful, doesn't seem to provide enough and strong enough advantages to warrant a language change, while it is also problematic (in terms of being future-proof) in the face of structs with private fields.

I don't find the motivation very convincing either. I understand that the proposed syntax is somewhat more convenient to use than writing a builder pattern or more than one constructor, but these mainly (with one exception) concern the person implementing an API, not those consuming it. The exception is of course the "config struct in constructor parameter" pattern, Window::new(WindowArgs{ width: 200, height: 200 }), as mentioned in the comments. While this is indeed a few characters longer than just saying Window { width: 200, height: 200, .. }, I specifically find it beautiful how such a simple pattern can solve a large spectrum of problems (including even eg. optional/default function arguments) without altering the language in any way. And I think this is more important than saving a few keystrokes.

@Centril
Copy link
Contributor

Centril commented Nov 4, 2018

@H2CO3

I'm not a fan of this. It makes sense, but how much more special will the Default trait be as a consequence of it? Parallel to recent discussions about From/Into, the beauty of these traits was that they didn't require language or compiler support so far.

This makes the Default trait and the #[derive(Default)] "macro" less, not more, special (or at least the impact is neutral). Simply put, the trait would by no means have to be a #[lang_item = "default"] and the "macro" would simply use the tokens provided by the TokenStream to implement Default for the trait.

(I know that #[derive(Default)] is in the compiler, but theoretically it could be in a library instead – it's not very highly coupled with the language, and it doesn't need to be.)

It couldn't because making it a library would be a breaking change; at most it can be a sort of quasi library proc macro that the compiler provides by default. In any case, if we implement this RFC, nothing changes. It can still be a library and the crate derivative and other procedural macros can make use of the provided defaults.

Why is that a symmetry? Conversely, they are doing two completely different things with the same syntax, not two aspects or "directions" of the same thing. Destructuring a struct and throwing away fields doesn't need extra information, there's nothing magical going on there, you just don't use certain fields.

"Magic" is in the eye of the beholder. When pattern matching a tuple with .., i.e. (a, b, ..) you are essentially having row polymorphism in the type system to deal with the pattern typing. That seems magical to me. But magic isn't always a bad thing.

I also feel that there's a sort of duality between pattern matching and ignoring the extra fields and constructing and providing extra fields. I don't think the usual "pattern matching follows construction" is violated here.

There are even nice laws here:

let x = Foo { alpha, beta, .. };
let Foo { alpha, beta, .. } = x;
let y = Foo { alpha, beta, .. };
assert_eq!(x, y);

Construction with default values, however, hides the fact that there is extra information required to instantiate a struct without specifying all of its fields. .. suggests that there's something "unimportant" or "ignorable" going on, whereas that is pretty much not the case – the user is supplying values without knowing about it. In contrast, .. Default::default() makes you know that you are supplying the values, while still keeping it simple (and syntactically lightweight).

I think there is something mostly "unimportant" and "ignorable" going on, otherwise I wouldn't use default values on fields. I also don't think .. Default::default() is syntactically light weight (relatively speaking) and it may also have side-effects... With .. I both know that semantic defaults (instead of merely structural defaults, which is what you ordinarily get with #[derive(Default)] otherwise...) are being used and that no side effects are happening behind my back.

I think that if defaults were being used when writing Foo { bar, baz } you would have a good case here and I would tend to agree, but in this case you have to write Foo { bar, baz, .. } so nothing is happening behind your back. You opted into it.

  1. The RFC also lists the ability to construct structs with private fields using the struct literal syntax as an advantage. I disagree – to me, that's a straight-up disadvantage. The reason is this. Most if not all structs with private fields have some sort of internal invariant, state, or other delicate property which must be maintained by their public API.

Sure, sometimes they do; however, even with .. being able to use the def_site's visibility instead of the call_site's visibility, you will likely still have other fields that don't have default and so you still cannot use Foo { .. } to construct a value of the type that may break invariants. For example:

pub struct Unique<T: ?Sized> {
    pointer: NonZero<*const T>,
    _marker: PhantomData<T> = PhantomData,
}

Unique { .. }  // Error! `pointer` is missing.
Unique { pointer: <value>, .. }  // Still, error! `pointer` isn't visible...

Changing the internals of how these types are supposed to work might sometimes be impossible without enforcing certain initial conditions on construction. For example, one might want to perform some checks in the constructor and make a decision base don them, or use values not local to the call site (that can't be, as such, specified once in the struct definition, for all future instantiations, much like there exist expressions which just couldn't be supplied as default function arguments).
This in turn means that a struct with private fields can transition from being able to be initialized using a literal, to the state of requiring a constructor function (e.g. T::new() and/or a manualDefault::default impl). This means that previously non-breaking private changes (ie. changes in the implementation of an already-existing constructor function) now leak to the public interface and cause a breaking change. This seems highly undesirable.

It is always possible to add a private field typed at some unit type; now the type cannot be constructed with Foo { .. }.

I don't find the motivation very convincing either. I understand that the proposed syntax is somewhat more convenient to use than writing a builder pattern or more than one constructor, but these mainly (with one exception) concern the person implementing an API, not those consuming it.

I think the language (+ team) should be concerned with making libraries (and libraries within applications) convenient to write.

@H2CO3
Copy link

H2CO3 commented Nov 4, 2018

It couldn't because making it a library would be a breaking change

Of course not now – but it could have been, that's what I meant.

There are even nice laws here:

I see that, but that's mostly ignoring the semantics. (Also just because it's mathematically appealing, it can have issues in practice, which I think this construct definitely has, as I mentioned before.)

I also don't think .. Default::default() is syntactically light weight

Sorry, then I think we are just disagreeing here.

and it may also have side-effects

So could this RFC with two of the proposed semantics. Only one of them prevents side effects; the others (allowing arbitrary expressions or only Default::default()) could also do everything nasty one could do within default.

It is always possible to add a private field typed at some unit type; now the type cannot be constructed with Foo { .. }.

Yes (but I doubt many users would know this or do it by default out of caution…), and then this part of the RFC completely loses its use anyway. By the way, this would now be yet another thing that programmers implementing an API would need to remember – "always add a mostly meaningless ZST private field by default, if you want to prevent construction via a literal". I think this is the wrong way around, as it makes the potentially dangerous situation the default, requiring one to opt out of it (and not even in a way that might be obvious to people).

I think the language (+ team) should be concerned with making libraries (and libraries within applications) convenient to write.

Definitely. I still don't think this particular RFC is justified. It's not like structs are hard or inconvenient to use today.

@Centril
Copy link
Contributor

Centril commented Nov 4, 2018

@H2CO3

It couldn't because making it a library would be a breaking change

Of course not now – but it could have been, that's what I meant.

But what value does the argument have if it's not actionable?
Furthermore, as I pointed out, it could still have been a library macro if we had made this change pre 1.0.

There are even nice laws here:

I see that, but that's mostly ignoring the semantics. (Also just because it's mathematically appealing, it can have issues in practice, which I think this construct definitely has, as I mentioned before.)

Yeah sure, just because there are mathematical laws doesn't mean it cannot have other problems, but then those need to be demonstrated (and I don't think sufficient problems have been...).
But mathematical beauty is a plus in my view. It also hints at a construct that is easy to understand.

and it may also have side-effects

So could this RFC with two of the proposed semantics. Only one of them prevents side effects; the others (allowing arbitrary expressions or only Default::default()) could also do everything nasty one could do within default.

I think we should discuss what the RFC actually proposes, and it proposes that the default value of a struct is a const context and thus the expression must be a constant expression. Those may not (at least not currently and that is very unlikely to change) have side effects.

It is always possible to add a private field typed at some unit type; now the type cannot be constructed with Foo { .. }.

Yes (but I doubt many users would know this or do it by default out of caution…), and then this part of the RFC completely loses its use anyway. By the way, this would now be yet another thing that programmers implementing an API would need to remember – "always add a mostly meaningless ZST private field by default, if you want to prevent construction via a literal". I think this is the wrong way around, as it makes the potentially dangerous situation the default, requiring one to opt out of it (and not even in a way that might be obvious to people).

I disagree that what you need to remember is "always add a mostly meaningless ZST"... I think almost always the other fields that don't have defaults are private (and then you wouldn't #[derive(Default)] anyways) so in that case it isn't possible possible to construct it via a literal anyways.

The language is set up in such a way that the default thing is to respect the principle of least privilege since fields are private by default. If your API is dangerous in some way such as if it involves PhantomData<T> + unsafe, then you are unlikely to have public fields. An example of where it could be dangerous is:

struct Id<A, B> {
    prf: PhantomData<(fn(A) -> A, fn(B) -> B)> = PhantomData,
}

impl<S: ?Sized, T: ?Sized> Id<S, T> {
    pub fn cast(self, value: S) -> T where S: Sized, T: Sized {
        unsafe {
            let cast_value = mem::transmute_copy(&value);
            mem::forget(value);
            cast_value
        }
    }
}

let refl: Id<u8, Box<u8>> = Id { .. }; // And we have bricked the type system.

but in this case you used unsafe so you should know what you are doing in any case.
I think these scenarios are unlikely to arise and I would make this trade-off personally.

@H2CO3
Copy link

H2CO3 commented Nov 4, 2018

But what value does the argument have if it's not actionable?

The point was that #[derive(Default)] doesn't need to be coupled with the compiler in principle.

I pointed out, it could still have been a library macro if we had made this change pre 1.0.

This is exactly what I'm trying to say (assuming by "it" you are also referring to #[derive(Default)]).

@Centril
Copy link
Contributor

Centril commented Nov 4, 2018

@H2CO3

But what value does the argument have if it's not actionable?

The point was that #[derive(Default)] doesn't need to be coupled with the compiler in principle.

Right, I think that's a worthwhile goal. But do you agree that this RFC doesn't change anything in this respect -- so the principle is retained?

With respect to how the principle is even enhanced, consider for example #[derive(Arbitrary)] for proptest, which we are working on shipping. It could make good use of the default values provided by the user. Ostensibly serde could do the same (ofc that is up to the authors of serde). For example:

#[derive(Debug, Arbitrary, Deserialize)]
struct Foo {
    // The proptest_derive macro will see this
    // and fix the value of `bar` to always be 42.

   // Serde will see this and assume that if no value is
   // provided then `42` will be used on deserialization.
    bar: u8 = 42,
    // other fields...
}

@H2CO3
Copy link

H2CO3 commented Nov 4, 2018

But do you agree that this RFC doesn't change anything in this respect -- so the principle is retained

I was asking if it does - and if it doesn't, I do agree it is retained.

@burdges
Copy link

burdges commented Nov 4, 2018

I'll tentatively disagree with the constructor .. here being "dual" to the pattern .., but that's easy to fix by using say ... here. In fact I'll use ... below because even discussing the feature as .. gets confusing.

If I understand, the ... proposed here ensures that values are instantiated by constants explicitly specified in the struct definition, while ..Default::default() executes arbitrary code distributed through arbitrarily complex type hierarchies. In particular, we cannot expand ... to instantiate a HashMap because doing so requires sampling random numbers.

I'd agree with @H2CO3 opposition to this feature if ... were merely a synonym for ..Default::default(), but.. I believe the restriction to constants explicitly specified in the struct definition makes this feature a real benefit code readers, auditors, etc.

I also think ... permitting only a partial default helps considerably with avoiding distracting boilerplate. In principle, we could achieve the same with structural records that support magical cross-struct FRU and some magical traits:

trait PartialDefault {
    type Part : Substruct<Self>;
    const fn partial_default() -> Part;
}

And this RFC's ergonomics can be recaptured with polymorphic constants:

const DEFAULT<T: PartialDefault> : <T as PartialDefault>::Part = <T as PartialDefault>::partial_default();

We could then simple write { foo , ..DEFAULT } instead of the { foo , ... } proposed here, but we've used similar magic to this RFC albeit general purpose.

@crumblingstatue
Copy link

My main problem with this feature is that it's a less generic solution than what could be achieved with constructor functions if Rust had named and optional args.

Quoting from the motivation:

The problem with a constructor is that you need one for each combination of fields a caller can supply. To work around this, you can use builders, like process::Command in the standard library. Builders enable more advanced initialisation, but need additional boilerplate.

This problem would be nullified if Rust had optional and named arguments.

Reasons why it's less generic than constructor functions

  1. Doesn't allow multiple sets of defaults
    You can have as many constructor functions as you want, and each can have a different set of defaults.
    With default struct field values, you can only define one set of defaults that are the defaults in all usage contexts.

  2. Doesn't allow non-constant expressions
    As stated in the drawbacks:

    Field defaults are limited to constant expressions. This means there are values that can't be used as defaults, so any value that requires allocation, including common collections like Vec::new(). It's expected that users will use a constructor function or builder for initialisers that require allocations.

    If constructor functions had optional/named arguments, they could be used in these cases with the same amount of convenience as default struct field values.

  3. Not applicable if you want to check for invariants at construction time
    For example, a Window could have a width field that is u32, but the actual width can't be larger than 1000000. Or you could have a struct with min and max fields, and min can't be more than max. A constructor function can easily check for these invariants.

I don't fundamentally oppose this feature, but I believe optional and default arguments would cover all the motivation for this feature, without the constant-only drawback, while also fulfilling other motivations.

As a closing statement, I want to acknowledge that optional/default arguments are indeed controversial, and they may take years to land, if ever. I just wanted to make sure this information is out there for people to consider.

@burdges
Copy link

burdges commented Nov 4, 2018

I believe structural records #2584 along with this feature provide precisely named and optional args since you could write:

fn foo(args: {a : bool = true, b: bool = false} ) { .... }
foo({ a: false, ...});

@Centril
Copy link
Contributor

Centril commented Nov 4, 2018

@burdges

I'll tentatively disagree with the constructor .. here being "dual" to the pattern .., but that's easy to fix by using say ... here. In fact I'll use ... below because even discussing the feature as .. gets confusing.

I don't mean "dual" in the literal category theoretical sense; but it does give such an "aura". This is sort of vague, but I hope you understand kinda what I mean.

As for using ..., that might be something we should do in a future edition (because ... already has a meaning elsewhere...) as a sort of revamp of FRU, slice patterns, etc. but for now I think .. is consistent with what we have and a useful improvement.

In particular, we cannot expand ... to instantiate a HashMap because doing so requires sampling random numbers.

This is true because of RandomState; However, if you change S to something different (e.g. fnv) then there should be no problem in principle with having HashMaps at compile time.

I'd agree with @H2CO3 opposition to this feature if ... were merely a synonym for ..Default::default(), but.. I believe the restriction to constants explicitly specified in the struct definition makes this feature favor code readers, auditors, etc. over code writers.

I think code writers benefit from this as well; it gives me great comfort when writing code to know that side effects are not being done behind my back.

@crumblingstatue

My main problem with this feature is that it's a less generic solution than what could be achieved with constructor functions if Rust had named and optional args.

Possibly, but the motivation isn't solely as an alternative to named and optional arguments (which are quite controversial on their own because of a host of problems particularly with the former...).

Quoting from the motivation:

The problem with a constructor is that you need one for each combination of fields a caller can supply. To work around this, you can use builders, like process::Command in the standard library. Builders enable more advanced initialisation, but need additional boilerplate.

This problem would be nullified if Rust had optional and named arguments.

But you would get other problems, for example, some variants on named arguments make function names part of public APIs and most variants of named arguments don't interact well with the trait system. Most floated solutions to optional arguments don't mesh well with how defaults work elsewhere in the language.

  1. Doesn't allow non-constant expressions
    As stated in the drawbacks:
    > Field defaults are limited to constant expressions. This means there are values that can't be used as defaults, so any value that requires allocation, including common collections like Vec::new(). It's expected that users will use a constructor function or builder for initialisers that require allocations.

The drawbacks are outdated. T-libs could make Vec::new a const fn on stable Rust tomorrow if it wanted to. I think most code should be possible in constant expressions in the future; the limitations that remain will mostly be around FFI, IO, and reading from global state. I think preventing those from happening behind your back is a feature, not a bug.

If constructor functions had optional/named arguments, they could be used in these cases with the same amount of convenience as default struct field values.

Yes, however, optional and named arguments does not enhance Default, does not make other procedural macros able to use the default values, so it is not panacea either.

  1. Not applicable if you want to check for invariants at construction time
    For example, a Window could have a width field that is u32, but the actual width can't be larger than 1000000. Or you could have a struct with min and max fields, and min can't be more than max. A constructor function can easily check for these invariants.

A constructor function can make use of the default values and not make use of others... You can still check the invariants that need to be checked if there are any. I don't think default values need to solve all problems. ;)

I don't fundamentally oppose this feature, but I believe optional and default arguments would cover all the motivation for this feature, without the constant-only drawback, while also fulfilling other motivations.

I have demonstrated, in this, and other comments, that optional + named arguments do not cover all the motivations.

As a closing statement, I want to acknowledge that optional/default arguments are indeed controversial, and they may take years to land, if ever. I just wanted to make sure this information is out there for people to consider.

Very fair! :)

@crumblingstatue
Copy link

crumblingstatue commented Nov 4, 2018

I believe structural records #2584 along with this feature provide precisely named and optional args since you could write:

fn foo(args: {a : bool = true, b: bool = false} ) { .... }
foo({ a: false, ...});

Making sure these two features work together to allow this use case is definitely worth looking into.

It would not cover all the use cases of optional and named args (most notably couldn't be used to give runtime parameters to functions because of the const-only limitation), but it could make some APIs more ergonomic.

Uhh, never mind the part about runtime parameters. Those only apply to the implicit defaults, the user could still explicitly provide runtime-dependent values.

Actually, I really like this idea. Even for those use cases that default struct field values don't cover, you could write your constructor function using structural records and default values combined.

If these features end up working together, they might be the answer to optional and default args.

@burdges
Copy link

burdges commented Nov 4, 2018

I'm endorsing the feature in what I wrote @Centril :

You'd want a Option<HashMap<K,V>> for most applications discussed here, but some crate could provide a lazily initialization wrapper that applies to HashMap, if one needs that. If you actually want a lazily initialized HashMap then crates can provide that functionality.

Also, easily readable code aids in correctness, while ergonomics favorable to writing code without being favorable to reading code tends to harm correctness. I'm mostly saying Bar { foo, .. } informs documentation reader that they can identify all default values in one click, to the struct declaration site, while Bar { foo , ..Default::default() } frequently leave them discouraged from pursuing the answers. In principle some Bar { foo, ..DEFAULT } might achieve this as well, depending upon how the declaration worked.

@nrc
Copy link
Member

nrc commented Nov 4, 2018

My main problem with this feature is that it's a less generic solution than what could be achieved with constructor functions if Rust had named and optional args.

I view this feature as a foundational step towards named and optional arguments. Named arguments should have struct-like syntax and desugar into structs of some kind. With such a feature, then default fields gives you optional arguments 'for free'.

@jplatte
Copy link
Contributor

jplatte commented Nov 5, 2018

This seems like a pretty useful feature, but I don't understand the rationale for allowing the construction of structs that contain invisible fields with it, especially given that FRU isn't supported for structs with invisible fields.
Allowing this even has an additional fallback over allowing it for FRU: It makes it easier to accidentally limit the evolution of the implementation of a struct by specifying defaults for all private fields. If one does that without considering this feature, adding or modifying fields in a way that they can't have defaults becomes a breaking change.

@richard-uk1
Copy link

@jplatte Maybe this feature could be combined with a non-exhaustive flag, so

#[non_exhaustive]
struct Options {
   first: bool,
}

would not allow instantiating the struct, similar to if you did

struct Options {
  first: bool,
  #[doc(hidden)]
  __private: ()
}

and

#[non_exhaustive]
enum Example {
  Variant1,
  Variant2,
}

is equivalent to

enum Example {
  Variant1,
  Variant2,
  #[doc(hidden)]
  __NonExhaustive,
}

This has been discussed before I think - there was talk of syntax like

enum Example {
  Variant1,
  ..
}

but I can't remember the details of the conversation.

@sbrl

This comment has been minimized.

@Lokathor

This comment has been minimized.

@dhardy
Copy link
Contributor

dhardy commented Mar 21, 2022

A lighter-weight alternative to this RFC is:

  1. Make ident: Type = expr valid syntax, but an error on code-gen
  2. Modify the #[derive(Default)] macro, or add another, to use these expressions in the implementation of Default, removing the value assignments (I already implemented this, but it doesn't work due to point 1)

Both are non-breaking changes, and would be sufficient to allow this:

#[derive(Default)]
struct Person {
    name: String = "Jane Doe",
    age: u32 = 72,
    occupation: String,
}

fn main() {
    let person = Person::default();
    assert_eq!(person.name, "Jane Doe");
    assert_eq!(person.age, 72);
    assert_eq!(person.occupation, "");
}

There are still issues here, but they're ones we're already quite familiar with:

  • derive(Trait) requires X: Trait bound on all generic parameters X, which is often wrong
  • partial constructors must use ..Default::default(), not just ..

Edit: ... and, this capability will soon land is in my new crate (impl_scope! required since field-initializers is not legal Rust syntax):

impl_scope! {
    #[impl_default]
    struct Copse {
        tree_type: Tree,
        number: u32 = 7,
    }
}

@scottmcm
Copy link
Member

scottmcm commented Apr 9, 2022

@dhardy Cool to have that in a crate!

I'd rather not do that plan in the language, though, since it'd make it harder to make those = exprs actually do something outside of macros in the future. I'd rather just give them meaning at the same time as making them parse.

@burdges
Copy link

burdges commented Apr 9, 2022

It's maybe handy if Default::default could be declared const fn ala https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4dc60134a2c8a128974dc9f307e9ae32 even though such code could not be generic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ergonomics Initiative Part of the ergonomics initiative final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. postponed RFCs that have been postponed and may be revisited at a later time. 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.