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

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 1506-adt-kinds - The Rust RFC Book

(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

Your concern is only for structs with no fields. Those can, as @scottmcm explained, already be initialized with various kinds of syntax, e.g.

struct Tuple();
struct Unit;

let t1 = Tuple();
let t2 = Tuple{};

let u1 = Unit;
let u2 = Unit{};

(playground)

Adding the possibility to write

let t3 = Tuple{..};
let u3 = Unit{..};

struct Struct{};
let s1 = Struct{};
let s2 = Struct{..};

isn’t going to hurt anyone.

There is a way to "opt out": Use #[non-exhaustive]. Or perhaps you meant something different, then you need to clarify what you mean by "opt out".


On a second thought, I’m considering to change my opinion on the implicit impl Default story. It could be a good idea. I don’t see any reason why a type that supports you writing TypeName {..} to obtain an instance of it should not implement Default. I would update the condition though. @ekuber wrote:

The condition should IMO be: "a struct that supports Struct {..} initialization (i.e. without specifying any field) implements Default automatically". I don’t see any disadvantages from this. Breaking support for Struct {..} is already a breaking change. This would mean: Types where all fields have defaults and the type doesn’t have non_exhaustive. Provided that we allow default initialization for private fields. If we don’t, then having no private fields would be another condition for automatic Default implementation.


Okay, after some more thinking, here is a problem:

The problem that could occur is with any form of "Voldemort types", since when you can’t write the type name you cannot write TypeName {..} either.

For example a public-in-private return type of a function:

mod m {
    mod private {
        pub struct S;
    }
    pub fn foo() -> private::S {
        println!("This will guaranteed to be printed for every S created!");
        private::S
    }
}

fn main() {
    let s = m::foo(); // can only obtain an `S` by calling foo()
    // cannot write e.g.
    // let s = m::private::S;
}

If S becomes Default, we can:


mod m {
    mod private {
        #[derive(Default)]
        pub struct S;
    }
    pub fn foo() -> private::S {
        println!("This will guaranteed to be printed for every S created!");
        private::S
    }
}

fn give_me_the_default<T: Default>(_: impl FnOnce() -> T) -> T {
    T::default()
}

fn main() {
    println!("Creating an S without calling foo!");
    let s = give_me_the_default(m::foo);
}

Perhaps as an intermediate solution, we can introduce a warning if a struct without non_exhaustive and with all fields having default values does not implement Default. The correct way to silence the warning without adding a impl for Default would be to add #[non_exhaustive].

3 Likes

As far as I understand rustc's internals, there is no such type available in the type checker, because partial initialisation (let x: T;) and partial moves are tracked by another system. So implementing your intuition would mean specifying a brand new family of types with new and interesting properties. (Just think about private fields of public types.) That's much more ambitious than just adding new traits and syntactic sugar for them.

2 Likes

Using attributes for denoting what should be and is already planned to be arbitrary constants does not seem idiomatic. It doesn't mix will with the other language portions for naming and computing constants. Then again, align has a similar problem :man_shrugging:.

Alternatively, we could "just" lint on structs where every single field has a default and it doesn't #[derive(Default)]. There might be value in making it explicitly opt-in, as they might be anticipating introducing mandatory fields in the future (but this might be doable with non_exhaustive or similar, but then we're expanding the language in a different direction than other features have :thinking:).


Wrote that before getting to your edit. I think we're in the same page.


You might notice that I was originally thinking that an unsafe trait PartialDefault could be the desugared version of this, but there were multiple reasons brought up for why it wouldn't work under the current Rust semantics. This doesn't mean that in the future this couldn't be made to work, and you are correct that we should consider the implications of the proposed impl on future changes we might want to introduce, but today it seems to me that the best course of action is to make it special (in the same way that const generics were stabilized for use in the stdlib before they are stabilized for general use, or how Try hasn't been stabilized but ? had a few iterations on how it actually operated under the covers).


I agree with @jhpratt and @scottmcm, we have very few instances of simple syntax hiding complex logic, and in the cases where we do that (binops, deref) there's "cultural" pressure to not be costly (sure, you could impl Add where using + on your type calls a remote server, but you'll likely get a flamewar named after you on all of the forums :sweat_smile:). Because of this, ensuring that initializing using .. people can be certain that the code isn't suddenly mining bitcoin has value, at the cost that if you ever do want to change to more complex code, the API will have to change. This is the same problem that leads to Java architects to provide getters and setters everywhere, and other languages like Python to have the concept of properties (method calls without arguments that look like field access). The former is a social solution to a technical problem, and it can be annoying. The later is a technical solution to a social problem (allowing evolution of public API without breaking semver) that brings along a bunch of uncertainty that Rust tries to avoid.

I think that the conservative position for .. that melds with the rest of the language design is for it to be more constrictive than less, and we can always expand it. Who knows, maybe we find that the init values don't need to be const and it could operate as if you were copying the code into the usage site (like a macro, but with potential access to private fields, maybe).

const parameters already have a settled syntax: literal values that can be distinguished at parse time are bare and you can use { /* more here */ } to disambiguate them from types otherwise (like for fn_with_const_param::<{ const_fn() }>(). I would like to keep as close to the existing code, and pretty much allow what a "naïve" user might try first.

1 Like

I think @quinedot is talking about this RFC which introduces const { expr } blocks where expr will be evaluated at compile-time (it desugars to an anonymous const item).

I'm worried not just about user-defined implementations but also it being called by users directly. If it's a safe trait method, then I can pass it as an impl Fn()->T, but actually using that result would have all the same problems as mem::uninitialized() that are why that got deprecated. While we have partially-moved-from types locally, we have no way to return such a subset from a function.

And looking ahead to user-defined implementations, the lack of a way to specify which parts need to be initialized also means that I can't see a way that the compiler could check that the struct literal provides the necessary fields.

So I think the wider context would need to be "way to return a partially-initialized object from a method, and a way to write that as a type that can be set as an associated type on a trait", which is such a massive feature that the "having a trait for this syntax" part is comparatively negligible, and thus fine to be done later.

I'm totally ok with considering this part of struct literal syntax, and would personally be fine if it was always just that -- one can always provide methods to wrap it in the more complex scenarios.

This conversation also made me think that this gives nice inside-the-crate update options, too. The same way one can choose to get errors on a field addition in a pattern by matching Foo { a, b } or choose not to get them by matching Foo { a, b, .. }, this would let applications (which tend to care less about semver) have that same choice for initialization -- Foo { a: 1, b: 2 } to get an error when adding a field, or Foo { a: 1, b: 2, .. } to not get an error so long as a new field is added with a default.

:+1: -- people are likely to already have other derives on these types anyway, so adding another one is a minimal problem and nice for lack of surprise.

And winapi might want to put default field values on its types in situations where it doesn't want to derive the trait because of compilation time issues.

2 Likes

Those are the rules for a position which must be const, such as an array length [usize; here] for example, correct? What will it look like if it's decided that the init values don't need to be const?

What I would hate to see is something like

struct Foo {
   // default field value expressions are assumed to be const
   a: usize = calculate_a(), // n.b. calculate_a is a const fn
   // but if you want you can opt in to a non-const field default
   b: usize = mut calculate_b(), // calculate_b is not a const fn
   c: usize = #[not_const] calculate_c(), // nor is calculate_c
}

Allowing both const and non-const default values without distinguishing between the two is an option. However it has the downside that you can't tell if partial initialization is available in a const context from the definition without checking each expression (e.g. looking up if the functions called are const or not). It would also mean that you could accidentally make partial initialization available in a const context (which would be a breaking change to take away later), unless there was some other way to flag "I don't want to guarantee these are const".

I was thinking about that RFC when I mentioned const { } (i.e. putting the default field value expression in an inline const { expr }, not the entire struct declaration in a const block).

If we allow something like struct Foo(u8, u8, u8 = 20, u8 = 30) for tuple structs, we might want to think about what’s a nicer alternative syntax for

let foo1 = Foo { 0: 0, 1: 10, .. };
let foo2 = Foo { 0: 3, 1: 12, 2: 21, .. };
// etc

The straightforward

let foo1 = Foo(0, 10, ..);
let foo2 = Foo(3, 12, 21, ..);

can’t work because of conflict with ops::RangeFull.

And

let foo1 = Foo(0, 10);
let foo2 = Foo(3, 12, 21);

is probably confusing / too implicit.

What is an acceptable syntax though?

Foo(0, 10; ..)?  Foo.(0, 10)?  Foo{(0, 10), ..}?  ⋯  we can get creative here.

Yes, sorry, that was unclear. I was also thinking of the feature implicitly implementing Default or related traits. I think that would be covered by your "a struct that supports Struct {..} initialization (i.e. without specifying any field) implements Default automatically" suggestion.

Note that an automatic Default implementation would be a breaking change for any fieldless struct that manually implements Default and doesn't specify #[non_exhaustive], unless the automatic implementation is "smarter" than #[derive] and avoids creating a overlapping implementation.

:exploding_head:

I am serious when I say, that's awesome, as well as the FnOnce approach, but it has the problem of it needing to desugar to as many fn foos as optional fields are elided, and one of the big advantages of the feature is that ordering doesn't matter for the struct init syntax. For tuples you can either elide the start, the end or use _ in the middle, but that's limiting to the point where I don't know if it is even worth it to include it as part of the initial RFC.

I think a bigger problem there is that Foo in that example is actually a "usual function call" https://rust-lang.github.io/rfcs/1506-adt-kinds.html#tuple-structs.

One solution here could be to say that struct Foo(i32, i32, i32 = 4, i32 = 5); desugars (roughly, because of pattern implications, but) to

struct Foo { 0: i32, 1: i32, 2: i32 = 4, 3: i32 = 5 };
fn Foo(_0: i32, _1: i32) -> Foo { Foo { 0: _0, 1: _1, .. } }

And yes, that means that you couldn't set those things through the function, but maybe that's ok and it'd be better to use names for them if you needed that anyway.

For examples of things where this would actually be fine and helpful, consider these:

struct Index<T>(usize, PhantomData<fn(T)->T> = PhantomData);
struct WordAlignedBytes([u8; 16],[usize;0]=[]);

(Of course, could also just not allow it with paren-structs for now.)


EDIT: Or, less seriously, it could desugar to

fn Foo(_0: i32, _1: i32, RangeFull: RangeFull) -> Foo { Foo { 0: _0, 1: _1, .. } }

so you'd need to call it as Foo(13, 42, ..) :smirk:

3 Likes

Why not just go all the way then

#[allow(non_upper_case_globals)]
static Foo: FooConstructor = FooConstructor;
#[derive(Copy, Clone)]
struct FooConstructor;

impl FnOnce<(i32, i32, RangeFull)> for FooConstructor {
    type Output = Foo;
    extern "rust-call" fn call_once(self, (_0, _1, ..): (i32, i32, RangeFull)) -> Foo {
        Foo { _0, _1, _2: 4, _3: 5 }
    }
}

impl FnOnce<(i32, i32, i32, RangeFull)> for FooConstructor {
    type Output = Foo;
    extern "rust-call" fn call_once(self, (_0, _1, _2, ..): (i32, i32, i32, RangeFull)) -> Foo {
        Foo { _0, _1, _2, _3: 5 }
    }
}

impl FnOnce<(i32, i32, i32, i32)> for FooConstructor {
    type Output = Foo;
    extern "rust-call" fn call_once(self, (_0, _1, _2, _3): (i32, i32, i32, i32)) -> Foo {
        Foo { _0, _1, _2, _3 }
    }
}

fn main() {
    dbg!(Foo(0, 1, ..));
    dbg!(Foo(0, 1, 2, ..));
    dbg!(Foo(0, 1, 2, 3));
}
1 Like