Pre-pre-RFC: syntactic sugar for `Default::default()`

The thing that comes to mind is that syntactically we could allow arbitrary expressions in the field default value, which would be checked later for being const, but way we could allow arbitrary expressions to be used for #[derive(Default)]... But accomplishing that would turn Default into something that wouldn't be able to be implemented by 3rd party libs in its entirety. I would leave this as "possible future work" that I do not expect to be done unless there's a lot of feedback about people wanting this once the rest of the feature gets heavy use.

:+1:; always good to start with tight restrictions since loosening them is easier than adding them later.

2 Likes

I wrote a macros for it https://crates.io/crates/default_macro. Not sure it works for all possible cases but definitely works for some :slight_smile:

2 Likes

An interesting bit of relatively-new prior art for having something like this, C#9 added "target-typed new" which allows constructing an object (including an object initializer, which does the what-people-initially-thing-FRU-does) without needing to type out its type.

1 Like

First of all, thank you @ekuber for putting the work into this RFC-to-be. I definitely think that the ergonomics around Default::default() can and should be improved.

I'm not so sure about the addition of a magic syntax though, instead of addressing some fundamental problems with the way that the ..Default::default() "operator" works.

(I also haven't read all comments on this, some of which seemed to be going in a very different direction from the actual RFC?)

In short: I think adding a new syntax sugar for nested Default invocations would feel inconsistent, if for example nested match defaults ( _ => ... ) are not similarly handled. Especially because ..Default::default() is quite recognisable, whereas ..* is really not. I also don't think that just shortening the function name is really going to be a big quality of life improvement. There might be some more possible one-liners, but in the grand scheme of things it doesn't really change anything.

Instead, I think ergonomic improvements around Default should attack the root of the problem, namely that partial default implementations need to be possible.

At least from interacting with other projects, and the projects I work on, the issue is usually having to repeat a lot of fields that should be default-able.

For example:

struct Config {
  path: PathBuf
  strict_mode: bool,
  version: u8
}

The problem with implementing a Default for this struct is that path will end up having to be an invalid value, and we have to hope that it is instantiated correctly by overriding the field with a better value (i.e. Config { path, ..Default::default() }). If we allow for partial Default implementations, there could be default values for strict_mode and version, but the type system will enforce giving path a concrete value before it is a valid struct.

Without special language support this could be implemented with a derive macro that generates a new struct where all fields are Option<T>, and a transformation from this temporary struct to the real struct, where a builder takes the set of inputs that is required to make it a complete struct.

With some language support this could potentially be much easier to use, and an elegant solution to avoid having stale fields provided by wrong (or incomplete) Default values on a struct.

This suggestion is a bit off what the original RFC-to-be proposes, but I think it's a much better solution to the underlying problem of what makes using Default so cumbersome.

Now, this wouldn't really make 5-nested structs where each has a default any less cumbersome, but I think it might encourage people to write their structs and builders in a way that makes it easier to not end up in these situations.

6 Likes

Thanks @spacekookie!

I know is a long thread so it is a good time to summarize what the RFC I'll be writing will propose, particularly because it is removed from what I originally wrote:

  • Language support for default values on structs through const expressions:

    struct Car {
        wheel_count: usize = 4;
    }
    
  • Language support for partially defaulted structs:

    struct Person {
        name: String,
        age: i128 = 0;
    }
    
  • Teach #[derive(Default)] to use the default values

    #[derive(Default)]
    struct Pet {
        name: Option<String>, // impl Default for Pet will use Default::default() for name
        age: i128 = 0; // impl Default for Pet will use the literal 0 for age
    }
    
  • Expand the "spread" .. operator to make the RHS expression optional, if it is not present it will use the default const expressions set in the struct definition:

    let person = Person { name: "Jane Austen".to_string(), .. };
    let person = Person { name: "Margaret Atwood".to_string(), ..Default::default() }; // Compilation error, Person doesn't impl Default
    let pet = Pet { name: None, .. };
    let pet = Pet { .. }; // Compilation error, name doesn't have a `const` default value
    let pet = Pet { ..Default::default() }; // Ok
    
  • Considerations around letting non-const expressions in the struct default are to be punted initially

  • Considerations around making partial defaults a user definable feature are to be left for a future RFC (explicitly not adding a new PartialDefault trait, at least for now)

  • Further conversation around "should a struct that has all default values set automagically impl Default?" and "should a struct that impls Default automagically work with ..?" is needed

  • fn default<T: Default>(t: T) -> T { T::default() } is tangential and independent of this RFC

  • I will be basing the RFC on prior work that has been closed in the past due to lack of bandwidth.

Please everyone, let me know if there's any item that I have forgotten in this quick summary.

23 Likes

In the case that only const-expressions are finally allowed it could be useful to have a ConstDefault trait to have way to initialize some types. Of course, that could be done later.

Also related, I think I have not seen in this thread any example with generic types. It could help to show in the proposal a PhantomData or similar.

The hope is to make this a more general feature that can apply to many traits (impl const Default, perhaps), because it would be suboptimal to end up needing ConstClone and ConstPartialEq and ConstAdd and ...

3 Likes

For the first point, this would affect structs without any fields at all as well. I don’t think that we want to have

struct Foo {}

suddently start implementing Default implicitly. Also, if an implicit Default impl was the case, it isn’t clear what the way to opt out it is.


On the second point (structs that impl Default automagically working with ..): This can also be postponed as it could be introduced later. Furthermore, this would already introduce a situation where the .. can have arbitrary side-effects (while the approach of only allowing const does not allow arbitrary side-effects from the .. syntax alone). Also this would be very confusing if only some fields of a struct have a default value, yet the whole struct implements Default. Will the behavior then switch between using the individual field default expressions vs the whole struct Default impl depending on what fields are initialized? E.g.

#[derive(PartialEq, Eq)]
struct S {
    a: i32 = 1;
    b: bool;
}
impl Default for S {
    fn default() -> Self {
        S { a: 42, b: true }
    }
}
fn main() {
    // so we’d agree that this works:
    assert_eq!(S{..}, S{a: 42, b: true});

    // but now also, one of the following
    // two assertiona has to be true as well:
    // either
    assert_eq!(S{b: false, ..}, S{a: 42, b: false});
    // or
    assert_eq!(S{b: false, ..}, S{a: 1, b: false});

    // which one? why?
    // the latter is more confusing, but the former introduces side effects
    // and thus potential overhead (e.g. allocations) from the `Default::default()`
    // call for fields that aren’t even used.
}


No idea which of these were perhaps already mentioned in the previous discussion, but I’m able to come up with four more points

  • We need a name for these default const expressions in the struct declaration. I mean a term to name this syntax. Something like "expression", "struct declaration", or "match arm". In your last post you called them "default values" which is an unfortunate term IMO since it could be misunderstood to mean the result of a Default::default() call. I’ll use something like "default field value declaration" for now... On that note, the StructName { field: expr, field2: expr2, .. } syntax needs a name, too. I’ll use something like "initialization using default field values" for now.
  • What about private fields? Can they have default value declarations? (Probably yes, since it’s useful for the Default implementation.) Then the question is: Can you initialize a struct with private fields by using ..? (Probably not for a similar reason to why functional struct update syntax doesn’t allow for private fields either, right)?
  • How does this interact with #[non_exhaustive]? I’d suppose that #[non_exhaustive] structs can not be initialized via initialization using default field values either. Sine #[non_exhaustive] should (as far as I understand) make it possible without breakage to add (public or private) fields to a struct. On the other hand, a setting where you only want to be able to add public fields with a default field value declaration without breakage is a thing you might want to be able to express, too. I.e. an annotation, e.g. #[non_exhaustive_with_defaults] that’s only really useful on structs without private fields. This would allow both initialization using default field values and functional struct update syntax to still be used. However it would disallow ordinary struct expressions (without ..) or patterns without ...
  • Are default field value declarations a useful idea for struct variants enums, too? How about tuple structs or tuple variants of enums? For the default field value declarations to have any effect on Default implementations for enums, we would need a standard-library-way to derive Default on enums first.
6 Likes

My preferred naming would be "partial const defaults" or "partial initialization defaults" for the feature. For the Foo { .. } syntax, "default field initialization" should work but does need to be differentiated somehow from the "spread operation" of "initialization using the values in the following binding".

I think we could only allow this for pub structs that have only pub fields, or for private structs, at least at first. But I think that code that can init the struct should be able to use the "default init" syntax, which means placing no restriction on the definition, only on the usage (if that). For the definition we could have lints warning about the non usage of the private default fields anywhere in the local module, if we were to not allow using .. to populate private fields. But I could also see an argument for allowing this (which can't be done with ..Default::default()). I would err to stick as close to the current behavior (you can't init priv fields with .. outside of its defining module).

I'm not sure I follow. non_exhaustive does affect patterns, where it requires you to use .. when matching. I would imagine that it would either operate similar for construction or completely ignore them (or have a flag to care about them #[non_exhaustive(ctor_too)]).

I think that is a follow up conversation to be had. An RFC for that will be needed independently of this feature.

That’s the one I called "functional struct update syntax" above, after this headline in the reference.

I’m assuming knowledge of this chapter in the reference here. In partucular:

Since functional update syntax is not allowed, I was assuming it makes sense that "initialization using default field values" would be disallowed as well. The reasoning being that otherwise e.g. Struct { field1: 123, ... } would be allowed; but then one could not add a new field that’s private or a field without an accompanying "default field value declaration", since such a new field would be breaking the Struct { field1: 123, ... } expression. Now if you consider this quote from the reference:

types annotated with non_exhaustive have limitations that preserve backwards compatibility when new fields or variants are added

then you see that it is supposed to be possible to add e.g. private fields to an non_exhaustive struct, hence initialization using default field values cannot be allowed.



In contrast, my suggested new annotation would have a weaker condition, e.g.

types annotated with non_exhaustive_with_defaults have limitations that preserve backwards compatibility when new public fields with an accompanying default field value are added

As a logical consequence, the restrictions would have to be as I stated:



I know that the name "non_exhaustive_with_defaults" is stupid. We’d need to come up with a better one.


Edit: I need to mention that all these thoughts regarding new attributes like non_exhaustive_with_defaults are orthogonal to this RFC. So I guess, don’t worry about any new attributes for now; my main point was just that one ought to mention, in a "reference-level" explanation, that for a non_exhaustive struct one cannot use initialization using default field values, i.e. an S { field: 123, .. } expression.

3 Likes

I would want default field values to work even with inaccessible (non-pub) fields, the same way my s! macro above works when the type has private fields. One can always just have private fields without defaults if one doesn't want it being constructed this way outside the module.

Putting defaults in private fields is materially different from the FRU problem -- it's easy to imagine defaults that make Vec { .. } work fine, whereas it's the non-defaults in Vec { ..v } that make it so that (current-)FRU on private fields cannot work.

That said, it shouldn't be allowed on #[non_exhaustive] types, since #[non_exhaustive] pub struct Foo {} shouldn't work as Foo { .. } everywhere. Some sort of #[non_exhaustive(bikeshed)] would probably be needed for that.

"Default field values", like the existing RFC, seems fine to me.

4 Likes

After some time thinking about this topic, default field value initialization for private fields seems more and more like a reasonable idea to me as well.

4 Likes

If we do it this way, what if we want to have a different default value every time? Like for date, can we have date: Date = Date::now()? I still think having #[default = xxx] and #[default_with = func] (or something else) may be a better choice for now.

I like this idea generally, but do have some concerns.

Avoiding conflict and division

As has been pointed out, having multiple standard ways of defining defaults leads to ambiguity/conflicts between the implementations. There is also possible ecosystem division (especially when the new version is more ergonomic and less costly than the old version).

I feel the best way to avoid conflicts and division is to make them part of the same system: "if you implement Default, the resulting value of fields must be the same as any existing default field value declarations". Other parts of the language already have relationships like this -- Copy and Clone is the main one that comes to mind (though there are others like Eq and Ord, etc.). Copy and Clone also has the precedent of giving ergonomic syntactic support to the potentially less costly version.

Future proofing

Next, I think future proofing is important, and am wary of .. being "too special", doing things which can not be easily extended or abstracted. In particular, I feel that the partially initialized structures should be fleshed out, so that there is some way to name and create them independent of default struct field value syntax (even if it isn't exposed outside of the compiler). This would keep the door open for partial initialization that doesn't rely on const values or default struct field value syntax -- e.g. a partial_default() which may not const or cheap, but also isn't complete. (Side note, the compiler already has a concept of partial structs due to partial moves on the local level.)

Similarly, I feel .. should actually be some sort of trait underneath (with special syntax support ala Copy, Deref, Try), even if the trait is not user definable to begin with. As a concrete example of why this is useful, consider tuple (or singleton) structs as mentioned by @steffahn:

// This works for all of `struct Foo;`, `struct Foo(atype);`,  `struct Foo { f: atype }`
foo = Foo { ..Default::default() };
// So I would expect this to be possible too
foo = Foo { .. };

For that to be possible, there needs to be some way to "impl .." for struct Foo; and struct Foo(atype);. It could be more special syntax (struct Foo =;??), but it would be cleaner if it were somehow part of the larger language instead.

Last point on future proofing: If we want to retain the possibility of non-const values in default field value position, perhaps the values should be prefixed with const or put in a const { }.

Considering Const and Partial orthogonally

After reading this thread, I'm thinking of the Default family of traits as having two axis: const or non-const, and partial or complete. Default currently supplies a complete non-const default, and .. as most recently sketched provides a partial const default. I feel it would be good to flesh out the missing half but also keep the axis orthogonal.

That would include ConstDefault with Default as a supertrait. That is definitely a generalize-able concept. It would be nice if there was some sort of Const marker trait like system, but don't know how feasible such a thing is now or in the future.

Finally considering "automatic Default based on ..", this would be a fourth auto trait; my instinct is the less, the better. Especially for a well-known, pre-existing trait. (Also in comparison, there is no automatic implementation of Clone if you implement Copy. Perhaps there's a history here that I'm not aware of?)

However, even if it is desired that Default be an auto trait, .. is the wrong "source". Partial is more general than complete; if these traits are to be kept in sync and auto-implemented, the more general trait should be auto-implemented in terms of the more specific.

  • Just PartialDefault implies no others
  • Just ConstPartialDefault (aka ..) implies PartialDefault
  • Just Default also implies PartialDefault (which happens to specify all fields)
  • ConstPartialDefault and Default together must agree; PartialDefault again follows from Default
  • ConstDefault implies all of the other three

Or in other words, supplying const default field values for all fields would implement ConstDefault (and the rest would follow), while only supplying some would implement ConstPartialDefault. If the const requirement is removed from default field values, the other two could be implied as well.

2 Likes

Personally I believe that is unidiomatic and should not be encouraged.

I have generally considered that Person { .. } not being able to do that is a good thing. I very often see people on this forum objecting to far more visible semantics than that, and would argue that it's probably better to do that by something that looks like a function call, not like a literal. (The fact that there are invisible things like Deref that can do arbitrary things notwithstanding.)

I agree, but I also don't think this needs anything more that the Copy and Hash examples you've already mentioned: a note in the Default documentation would be sufficient.

I'm not concerned about this one because it's substantially-easier to have Default be consistent than Ord+Eq+Hash. Namely, the derive will use the defaults in the definition, so the easiest way to be consistent is just to put them in the definition and use the derive. And if you need more than that, being lazy in a manual implementation of default will do the right thing -- Foo { x: get_random_number(), .. } that only mentions the ones without defaults in the definition is the shortest-and-simplest way to write the body, and is consistent.

Can you elaborate on things that could not be done by adding the abstraction later?

It would be, regardless of whether it was defined as struct Foo { x: i32 = 10 } or struct Foo(i32 = 10); or struct Foo;, thanks to https://rust-lang.github.io/rfcs/1506-adt-kinds.html

(Like how today one can initialize struct Foo(i32); with struct Foo { 0: 10 }.)

What type do you see this trait producing?

2 Likes

What I'm really trying to advocate here is that the feature be based on the building blocks of the language, so that (say) partially initialized structs aren't that unique thing only produced when a definition with const default field values exist. They seem useful independent of const default field values. Or as another example, a const default() (for all field values) also seems quite useful. But it would seem odd to me if to achieve this (in the canonical fashion) I had to rewrite a trait impl as field defaults; could not define it on ForeignStruct<MyType>; could not specify it as a trait bound; etc.

The feature could be a special case to begin with and extended or abstracted later, sure. But extensions may be harder or uglier to achieve if they aren't considered up front. The sketch said that considerations around non-const default field values are to be punted initially, and considerations around making partial defaults user definable to to be left for a future RFC. I'm saying those both seem desirable and they should be under consideration now -- not for implementation as part of this RFC, but as a viable future direction.

Right, but if the partially initialized constructor .. is only possible via const default field value definitions, what's the syntax to make .. available to tuple and unit structs?

struct Foo { field: usize = 42 } // <-- makes `..` available
my foo = Foo { .. };

struct Bar; // <-- what do I do here
struct Quz(usize); // <-- or here
// so that these are possible
my bar = Bar { .. };
my quz = Quz { .. };

(If partially initialized constructors were defined in terms of a trait, one answer would be "implement the trait".)

I reached for traits as that seems to be the precedent (Try, Copy, Unpin), and the feature is naturally tied to Default. I'm not sure I understand the question though; what kind of type does the Try trait produce?

1 Like

That's the same as struct Bar {} const Bar: Bar = Bar {};, and it's not marked #[non_exhaustive] (thus can never get any fields without it being a breaking change anyway), so Bar { .. } would work because it would default all zero omitted fields. (Like how one can have a .. in a pattern that doesn't actually match any fields.)

struct Quz(usize = 42); seems logical. If the braced form can change from type, to type (= value)? , then the same should work fine in the parenthetical version.

But Default is -> Self, and this cannot produce a fully-initialized value. It maybe could be -> MaybeUninit<Self>, but that's essentially impossible to use generically since there's no way to know what still needs to be initialized. Unless you were thinking it'd generate something that takes a tuple of the non-defaulted fields, or something?

So it's automatic for those. Is there a way to opt out of it? (The concerns past this point are basically the same as @steffahn already raised for struct Foo {} interacting with an automatic Default impl.)

Oh, now I understand -- what type does the function within the trait return. It's a "Self with unitialized fields". It's the same type that happens when you move a field out of a struct. It could be the same type as MyStruct { .. } when there are fields without defaults.

I'm afraid I don't have a syntax or implementation in mind to share other than spitballing. It doesn't even need a firm syntax until/unless it's decided to allow user defined implementations. But I am advocating fleshing the concept out in a context wider than this one new feature.

(Spitballing: Perhaps it returns a MyStruct { .. }, and different implementations are different types ala closures. Unitialized fields are tracked at the call site analogously to how structs with partially moved fields are tracked. Or maybe it returns a MyStruct { this_field, that_field, .. }.)

1 Like