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

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

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