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

I was originally envisioning this as pure sugar for Default::default() but reading the feedback I think it'll be worthwhile to tackle partial initialization first so this sugar can be used for that. I'm picturing the following:

#[derive(Debug)]
struct S {
    a: usize,
    b: usize = 42,
}

fn main() {
    let x = S {
        a: 42,
        ..
    };
    println!("x {:?}", x);
    let y = S {
        .. //~ ERROR field `a` must be specified
    };
    println!("y {:?}", y);
}

being sugar for something along the lines of

#[derive(Debug)]
struct S {
    a: usize,
    b: usize,
}

trait PartialDefault {
    unsafe fn partial_default() -> Self;
}

impl PartialDefault for S {
    unsafe fn partial_default() -> Self {
        S {
            a: std::mem::uninitialized(),
            b: 42,
        }
    }
}

fn main() {
    let x = S {
        a: 42,
        .. unsafe { S::partial_default() }
    };
    println!("x {:?}", x);
    let y = S {
        .. unsafe { S::partial_default() }
    };
    println!("y {:?}", y);
}

where the compiler verifies that the uninitialized fields are provided.

We would also then be able to provide a blanket impl<T: std::default::Default> PartialDefault for T {}, so the new syntax would work then be equivalent to calling ..default(), and extend the #[derive(Default)] macro to understand the field: Type = <const value>, syntax so that you could write

#[derive(Default)]
struct S {
    a: usize = 42,
    b: Option<usize>,
}
5 Likes

Ouch, that's a whole lot of cleverness for such a small convenience thingy.

Other than that, there's a concrete implementation problem with the above: is it possible at all to move an uninitialized() value, given that even reading such a value is UB? If not (as I suspect), that would impede a trait-based desugaring and require core language support (ouch 2.0).

2 Likes

Anything involving mem::uninitialized() makes me skeptical at best, since it's definitely UB for a bool and is at least strongly discouraged even for usize.

A proc macro for something like that would, I think, need to generate a copy of the type with all the fields wrapped in MaybeUninit<_>. And I'm not convinced that's generally valuable.

So long as the //~ ERROR field `a` must be specified part works, I think the compiler will need to know enough that I'm not convinced that going through a PartialDefault trait is worth it.

That part I totally agree. Having the proc macro generate

impl Default for S {
    fn default() -> Self { 
        Self {
            a = const { 42 },
            b = Default::default(),
        }
    }
}

seems completely reasonable and almost-no-magic.

And given that serde has attributes for defaults already, I can see this being valuable information for other proc macros too.

(The only weird consequence that comes to mind would be that it would be possible to manually impl Default in a way that doesn't match the defaults on the fields, but that's no worse than existing cases like inconsistent Eq/Ord/Hash. And the compiler can provide nice hints that you should derive Default if you have defaults for all the fields.)

So S{a : 8, ..} could be given a meaning of invoking one of

  • Default::default()
  • Create::create()
  • PartialDefault::partial_default()
  • or compiler substituting const defaults from struct declaration

The last solution does seem to have a couple of advantages:

  • it's easier to learn/teach/reason about
  • it permits const expressions of struct types

Re unsafe - I believe just constructing a bool with an undefined value is theoretically UB.

P.S. Another minor point: defaults in attributes will be less ergonomic than struct S { a : bool = true }

I think that most of you are aware that C++ introduced exactly this. Being able to set a default value for an attribute directly in the struct/class declaration allowed to factorize the implementation of the various constructor, often allowing to use the default implementation. I personally think that this would be the best solution.


In the rare case where there is both a default value for some attributes, and Default is implemented, I think that using const defaults from struct declaration is the best solution, because it allow for a clear implementation and nice diagnostics:

struct Color {
    r: u8,
    g: u8,
    b: u8,
    alpha: u8 = 0,
}

impl Default for Color {
    fn default() -> Self {
        Color {
            r: 0,
            g: 0,
            b: 0,
            .. // use the default value specified in the struct declaration
        }
   }
}

fn use() {
    // we can use the value specified in the struct ...
    // Note: to compile all value not specified in the struct declaration must be explicit

    let _ = Color{r: 255, g: 0, b: 0, ..}; // a solid red color
    let _ = Color{r: 255, g: 0, b: 0, alpha = 128}; // a half transparent red color

    // .. or we can use Default::default() explicitly

    let _ = Color::default(); // a solid black color

    // In case of ambiguity, Default::default() must be explicit

    // let _ = Color{..}; // does not compiles
    // let _ = Color{r: 255, ..}; // does not compiles
    // let _ = Color{r: 255, aplha=128, ..}; // does not compiles

    let _ = Color{r: 255, ..default()}; // a solid red color
    let _ = Color{r: 255, alpha = 128, ..default()}; // a solid red color
}

Note: when manually implementing Default, if some fields have already a default declared in the struct definition, a clippy warning could warn against specifying another value.

impl Default for Color {
    fn default() -> Self {
        Color {
            r: 0,
            g: 0,
            b: 0,
            alpha: 255, // warns: default value for alpha is 0
        }
   }
}

If both Default and the const value specified in the struct definition match, another warning could advice to use .. instead

impl Default for Color {
    fn default() -> Self {
        Color {
            r: 0,
            g: 0,
            b: 0,
            alpha: 0, // warns: default already specified in the struct, you should consider using `..` instead
        }
   }
}
6 Likes

A bit of bikeshedding, but if this functionality is restricted to const defaults specified in the declaration, then ..= could be a useful alternative token for the syntax, mirroring the = from the declaration, and perhaps helping to disambiguate from an otherwise perhaps assumed ..default::default().

canvas.polyline(&path, PaintOptions { stroke_color: Color::red(), ..= });
1 Like

That's interesting. Another fun thing about it is that { .. } is a valid expression today, but { ..= } isn't.

There's a few things that make me not super fond of it, though:

  • Most importantly, patterns already use .., so paralleling that needs something quite persuasive to move off it.
    • Hmm, actually, I suppose that ..= could only match the defaults of those fields in the pattern too. (So while Point { x, .. } matches Point { x, y: _ }, it would mean Point {x, ..= } would match Point { x, y: 0 }.) I don't know if that'd be useful, but it's at least interesting.
  • It's not obvious to me that the connection make would be to the =s in the struct definition as opposed to RangeInclusive. ("Oh, is this literal inclusive somehow?")
  • This would be a situation where if they feature is successful it may end up being used all over the place, so it's one of the few places where I actually care about it being short and trivial to type.
8 Likes

Personally i'm not much of a fan of ..=, my first pythonic impression is that it "assigns" something of the matter, but the open-ended assignment operator doesn't sit well with me, and feels a bit 'unstable' in the wake of nothing being placed after the operator.

Alternatively, possibly .=... or ..=...? With .= being an assignment operator that takes in "defaulting" values, the fields that're omitted, and ... being syntactic sugar for Default::default()

This way, a struct Value{a: i32, b: i32, c: i32} could be created with Value{ .= -1 }, assigning -1 to every field, the given value after .= would require to be the same type, or have Into for the assigning types for this to succeed, or be Default::default(). (or, alternatively, this same suggestion, but using ..= as the operator in question)

1 Like

At that point, I'd rather type English than Morse code (..=...). With the proposed free-function, default() is not much longer, and imposes a lot less mental pressure.

8 Likes

Not to mention the churn that inclusion of a new multi-glyph operator (such as ..=...) would induce in Rust's lexer, parser, and support tooling, including 3rd-party tooling such as IDEs and published teaching materials.

3 Likes

As an aside, I dislike the fact that currently .. is overloaded to mean both a range constructor and functional record update. Even though the grammar is technically unambiguous, to a human reader it's a bit confusing to see ..foo and not be immediately sure whether it's a range literal or FRU. Now that ... is no longer a range operator even in nightly, it might be possible to adopt it for FRUs (and other potential variadic-like uses), and have .. and ..= be range constructors and nothing else.

7 Likes

Given the conversation I feel like there's enough meat here to propose 3/4 different RFCs with varying degree of agreement/desire:

  • Extend #[derive(Default)] to accept explicitly setting the default value inline on the ADT definition
  • Use the same syntax as above to define a non-usable representable way of defining default values for a subset of fields which is treated specially by rustc as a language construct. This would also require some syntax (being purposely ambiguous here due to the understandable likely long bikeshedding on the specifics) to be able to represent ..compiler_intrinsic_partial_default.
  • Potentially expand the above to be representable by users with some trait
  • After fn default<T: Default>() -> T { Default::default() } and the second item above are stabilized, consider using allowing that special syntax to also be used if Default is implemented.

Of all of these, the first one seems like enough of a clear low hanging fruit and simple win that we should go ahead and start with it. My preferred syntax of field: type = const_expr faces the problem that derive macros do not modify the token stream they are fed (the compiler can do icky stuff, but I would like to avoid doing more magic than necessary), so we either promote that syntax to something representable in the AST that produces an error if #[derive(Default)] isn't used, or we introduce this feature with field attributes (which in my eyes looks worse and we would move away from if the second feature makes it in).

2 Likes

I agree with the above, but I also think it'd be fine to propose a default-syntax like .. in parallel that just uses Default, with an eye towards forward-compatibility with partial defaults. That doesn't have to wait until the last step of the plan you're describing.

3 Likes

I think this is exactly @KodrAus's https://github.com/rust-lang/rfcs/blob/7c55f73adb1c3ffdf3ae9decbe8af8d8bbe06d6f/text/0000-default-fields.md -- it even requires that the defaults be const expressions, which is much more usable now than it was in 2016 when first written.

It was almost merged three years ago, but ended up getting postponed due to the pre-edition impl period: https://github.com/rust-lang/rfcs/pull/1806#issuecomment-327922562

Maybe it should just come back?

(T-Lang zulip conversation about revisiting it.)

10 Likes

Does this mean that Default now has to be a lang item?

1 Like

No. It means that the Default proc macro in the library (well, technically in the compiler right now, but conceptually is in the library) needs to read those expressions from the token stream.

(If you don't want Default to be a lang item, you might be interested in #77688.)

4 Likes

11 posts were split to a new topic: Making Default a lang item so that function pointers can implement it

It certainly seems to keep coming up in discussion. Is there procedure for reopening a paused RFC unchanged, if the "post" it was phoned to happens to be now? Would it fall on e.g. @ekuber to lead the charge if @\KodrAus is no longer available?

Since the content of RFCs is usually dual-licensed under MIT/Apache-2.0, I guess that anyone can submit the same RFC again.

However, the new RFC should also explain what has changed since the previous RFC was closed/postponed, that justifies merging it now.

So something along the lines of

This PR reopens <link to last one>, which was postponed due to edition crunch. Besides [ongoing](link to @Centril's [DRAFT] RFC: Default field values by Centril · Pull Request #19 · Centril/rfcs · GitHub) [interest](link zulip) [in](link here) the feature, and those outlined in the original RFC, motivations are

  • The original closure reason no longer applies
  • const expressions are much more usable than they were in 2016, making the restriction less limiting.
  • there seems to be rough agreement on specific Struct { a, b, .. } syntax.
2 Likes