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

I still don't get why we can't use Haskell way for this problem - include a def() function in Prelude, that is polymorphic on return type.

That's what the briefly discussed fn default<T: Default>() -> T { Default::default() } is, which is enough for the original proposal. But there's interest in also have support for partial defaults which would require language level support, not just a function or trait.

1 Like

The problem is that since Default::default() is not const (and runs arbitrary code), this doesn't have the same behaviour as possbile partial defaults which are const expressions. As mentioned here:

I think the question is, what are the use-cases? The most concrete one I saw here are "options"-structs, which benefit mostly from partial defaults and support for #[non_exhaustive] structs. In the meantime, I think a free-function default gets most of the benefits of a mid-term solution, while being cheap to implement and avoiding potentially introducing conflicts if the { .. } syntax (as opposed to { ..default() }) will be adopted for the full version.

2 Likes

I also think that Foo { a, b, .. } should expand to the const expressions specified in the struct declaration, while Foo { a, b, ..default() } would use Default::default(). Furthermore, if both a and b are const, then Foo { a, b, .. } would be const too, but the not Foo { a, b, ..default() }.

3 Likes

It seems reasonable to make this distinction (and the possibility of sometimes using .. to imply ..default() when possible could be left as potential future work, but doesn't need to ever be done). The only concern I would have then is teachability: there are two slightly different ways of doing the same thing with different trade-offs.

What would be the behavior on the definition side?

#[derive(Default)]
struct A {
   b : Option<u8>,
   c : u8 = 43; // has to be a const expression
}

Would mean that this would impl Default for A and would also have a way of instantiating it with A { b: None, .. }? I would be ok with it (and we could taper of the sharp edge of calling A { .. } with appropriate diagnostics).

BTW, we cannot in good conscience cause the above definition to not work with the partial constructor syntax because derive macros are not capable of rewriting their token stream (we potentially could with Default because it is an intrinsic, but we very much shouldn't).

1 Like

Thinking about diagnostics, there are a few cases to consider:

  • using A { .. } when all fields are not defaulted to const expressions
    • has Default suggest ..default()
    • always, suggest fields that require values, if those impl Default, then the suggested placeholder expression would be Default::default()
  • A { ..default() } when it doesn't impl Default
    • has some const fields, suggest A { non_const: <expr>, .. }
  • A { ..binding } when binding doesn't exist, same as first bullet point

Should the following imply #[derive(Default)]? I think it might, but with the suggestion mentioned above it wouldn't be needed.

struct A {
   b : Option<u8> = None,
   c : u8 = 43;
}
3 Likes

I think not, and I'd justify that by how we handle Copy -- you can copy all fields with A { ..*ref_a } even though A does not implement Copy. I think we can say the same for Default, that having defaults on the fields is distinct from Default on the whole.

4 Likes

I agree with cuviper that probably not. I think I'd expect to see it in the derive list.

My first instinct is that I'd be in favour of a lint suggesting it, though, since I think that if A { .. } works then there's no reason to not also have A::default() work.

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.

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