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

I also hope I can use _ { field, field2, ..default() } someday.

I hope someday there will be default values for struct fields and it will be possible to just write: _ { field, field2 }

What about something like;

  • ...!
  • ..etc or ...etc
  • ...

(I'm new here, but I'm trying to bring in some new perspective to this idea.)

The .. syntax would be an unprecedentedly implicit way of calling a function (or even multiple functions if we consider the "default the rest of the fields" feature). Initializing a struct's field with a default value is often incorrect, so I think this action should be more explicit and noticeable.

If default values were limited to constants it wouldn't be that bad would it?

If anything more complex is required a crate could provide a constructor and/or Default::default

1 Like

Oooo, so it could be a way to call a const default? I like that a lot actually.

I thought this branch of discussion was about a possible alternative to Default::default:

// possibly some new annotations inspired by but not equivalent to
// #[non_exhaustive], for example
//
// #[deconstuctor_required(a)] - it's okay to skip all fields apart from a in
// let Test{a : a} = t;
//
// #[constructor_required(a)] - when constructing Test only a is required
// it duplicates info in struct definition but serves as belt and braces for API stability
// because it's easy to remember that this can only be extended on a major version bump
// it's an error to include a field outside of what #[constructor_required] prescribes
// without giving it a default value
struct Test {
  a : u8,
  b : u8 = 14, // has to be const?
}

...
let t = Test{a : 15} // b : 14 implied
1 Like

I don't know. Making Default into another special trait and introducing some special syntax feels like a pretty high price.

I mean, yes, it is annoyingly verbose, but Rust is somewhere on the complex end of spectrum already and every bit of syntax added is a bit of syntax to learn. There are languages that don't have syntax sugar for error handling even O:-).

Having things implied sounds… a bit scary to be honest.

4 Likes

I would never want to have fields implicitly filled in if I hadn't acknowledged that in the definition. , .. would be a reasonably short acknowledgement of that.

7 Likes

I was worried about this in the past, but I think const is helpful here, since it's set a bunch of precedent about what kinds of things are less scary, and we could require that defaults are const expressions.

(The part of this that's #[derive(Default)] looking at the extra tokens for the default value and using those in the emitted impl Default seems perfectly reasonable, at least.)

1 Like

It does get a little awkward with .. though.. Suppose a crate exposed

pub struct Options {
   pub a : u8,
   pub b : u8,
}

As I start using it there is yet no reason for me to write let o = Options{a : 0, b : 0, ..}. But then it seems reasonable for the crate author to be able to do this without a major version bump:

pub struct Options {
   pub a : u8,
   pub b : u8,
   pub c : u8 = 0;
}

There seem to be two ways out:

  • allow fields to be filled out implicitly w/o ..
  • make this an opt-in

This opt-in annotation could also serve as a belt and braces mechanism to ensure API stability:

// with this annotation it is an error to provide defaults for either a or b
// it is also an error not to provide a default for c
// the advantage of such a complex annotation is that it's easy to remember
// not to extend it without a major version bump
// it can also serve as an opt-in for not using .. in
// let o = Optons{ a : 0, b : 0 }
#[constructor-required(a, b)]
pub struct Options {
   pub a : u8,
   pub b : u8,
   pub c : u8 = 0;
}

P.S. perhaps it's easier to learn without the annotation :slight_smile:

I think the author in that example should have declared Options as #[non_exhaustive] in order to make this a valid minor version bump, which in turn would require the .. in the instantiation site.

Incidentally, IMHO using .. as a syntax shorthand for #[non_exhaustive] would create a nice symmetry.

pub struct Options {
    pub a : u8,
    pub b : u8,
    ..               // non-exhaustive, instantiations must use .. as well
}
6 Likes

That was contemplated in the non_exhaustive RFC discussion, see the mention in 2008-non-exhaustive - The Rust RFC Book

3 Likes

I would never want to have fields implicitly filled in if I hadn't acknowledged that in the definition. , .. would be a reasonably short acknowledgement of that.

So you're against default values of struct fields or that it should be visible for the reader when they're used?

In all the years using languages with default values for struct fields or default values for function parameters, I really can't say that they've caused major issues for me.

At the end it's all about API design, when it makes sense to have default values and the choosing of sensible values. A default value should in all cases make sense and never result into bad behavior.

It would help to avoid the performance pitfall, but the correctness pitfall would remain.

Initializing all remaining fields with default values using .. is dangerous because it's unclear which fields will be affected, so it is way too easy to accidentally initialize an important field to 0, an empty vector, or other nonsensical value.

The current ..Default::default() is also a bit dangerous, but at least it only works if you've implemented/derived Default for the entire struct, acknowledging that there is a correct default value. However, using Default implementation of individual fields can backfire easily and is very much like assigning default value to uninitialized local variables (which doesn't exist in Rust). For example:

let x = sensible_initial_value();
MyStruct { x, another_field }

Rust protects you from forgetting to initialize x in this case, so you can't just remove = sensible_initial_value(). Now, consider this example:

MyStruct {
    x: sensible_initial_value(),
    another_field,
    ..
}

If the proposed feature existed, omitting x: sensible_initial_value() would initialize the x field to the default value of its type, which is effectively the same error that Rust protects against in the previous example.

This syntax also already exists in patterns, so it doesn't look too suspicious: in patterns, it can cause you to lose data, but it will never give you invalid data. That could lead to overlooking the danger of the same syntax in struct initializers and creating hard to spot errors.

Ah that's the misunderstanding I guess :slight_smile: The proposal discussed is to use the default value - if provided - from struct definition:

struct Options {
   a : u8,
   b : u8,
   c : u8 = 43; // has to be a const expression
}

...
let o = Options{a : 1, b : 1, ..}; // c : 43 implied
// this wouldn't be legal:
// let o = Options{a : 1, ..};
// as there's no default for b in struct definition
2 Likes

I care about the latter. Default values are fine, but I always want Struct { a, b } to give me an error if there are fields other than a and b.

10 Likes

I've been watching this topic for a while, and thinking about what feels best.

I personally really like the idea of Struct { a, b, .. } being shorthand for defaults, if and only if Struct opts in. The meaning seems recognizable based on already-existing syntax – we know that .. means "fill in the rest of the fields from somewhere", and if there's nothing specified, "...from the defaults" is the only possible meaning.

The thing I'm most unsure about is the pros and cons between "it's just syntactic sugar for ..Default::default()", and "add a new syntax to allow struct to specify defaults for individual fields".

7 Likes

I feel pretty strongly that it should be the latter right now:

  • We don't yet have a way for Default::default() to be const, and it would feel weird to me for Foo { a, b, .. } to be non-const sometimes. Whereas Foo { a, b, ..default() } being non-const because default() is non-const seems eminently reasonable.
  • Similarly, Default::default() can be arbitrarily-complicated right now. I would thus expect rational pushback against "hiding" it inside the .. -- having this go and do something like read /dev/random because of one of the unmentioned fields does that could easily be surprising.
  • ..default() FRU is heavily restricted right now: all the fields must be accessible and it's can't be Drop. Neither of those restrictions are things that we'd want (or need) for this syntax.
  • ..default() needs to initialize all the fields, even the ones that are then overridden. The optimizer will probably be able to remove that, but it would be nice for it to not have to.
12 Likes

As another alternative we could have auxiliary anonymous structs implementing Into<MyActualStruct> for any MyActualStruct that can be filled with defaults. So that we write something such as

let o:Options = nameless_struct!{ a : 1, b : 1 }.into();

Actually, I think I prefer others of the alternatives already presented, but perhaps this one can help.

No, it is not at all unprecedented:

  • for loops implicitly call IntoIterator::into_iter
  • The ? operator implicitly calls Into::into
  • The . might implicitly call Deref::deref, possibly multiple times

In practice, this "implicitness" has never been a problem, as far as I'm aware. These traits can, theoretically, have arbitrary side effects, but in practice, I've never encountered an implementation that does. I think it's a weak argument that "hiding" a function call in .. is bad, because the function might read from /dev/random/. If it does, then it is poorly written code, and you shouldn't use it in your project anyway.

That said, I don't think making .. a syntactic sugar for Default is necessary, because ..default() is already sufficiently concise. If you disagree, you can rename it to make it shorter:

use std::default::default as def;

In my opinion, the .. syntax (without a function call) should be reserved for a more powerful syntax that allows both optional and mandatory arguments. Here's a rough sketch how it could look like (playground):

trait Create {
    type Mandatory;

    fn create(mandatory: Self::Mandatory) -> Self;
}

It could be easily implemented with a derive macro:

#[derive(Create)]
struct Options {
    a: i8,
    b: u16,
    #[default] c: u8,
    #[default(42)] d: u8,
}

// this expands to the following:

impl Create for Options {
    type Mandatory = (i8, u16);

    fn create((a, b): Self::Mandatory) -> Self {
        Self {
            a,
            b,
            c: Default::default(),
            d: 42,
        }
    }
}

Options { a: 1, b: 2, c: 3, .. } would then somehow have to be desugared to

Options {
    c: 3,
    ..Create::create((1, 2))
}

This is just a prototype, I'm not sure if it would work in practice. Note that a and b are mandatory, omitting them should trigger a compiler error.

3 Likes