Pre-RFC: User-provided default field values

Sorry to be negative, but I think that the costs from adding any new language feature outweigh the benefits here. I am not even convinced it's an improvement, it's one more thing to be learned for anyone learning the language ( including the restriction to constants ).

Generally speaking, I think the bar for new language features should be set high, and this proposal does not clear that bar.

2 Likes

The feature of being able to skip initializing some fields in a constructor expression is quite useful, I believe. The field: Type = value syntax the seems quite appropriate for such a language feature, and once this feature exists, it could be a useful idea to be able to use this syntax also for some macros.

Under this view, I would agree that without the default field values feature that allows you to do let ascribe = ExprType { expr: make_expr(), ty, .. }; style initialization, it's quite reasonable to seriously question the introduction of any new syntax.

Now assuming that we do get default field values, it seems like a reasonable logical thing to do to separate that proposal from the usage of the syntax in macros, and only do the latter as a subsequent proposal/feature. However, that's not possible if we want behavior such as

#[derive(Default)]
struct Foo { field: u32 = 42 }

to use the value 42 for field in the Default implementation, instead of the value u32::default(). Since we don't want to introduce breaking changes, the only way to introduce field default values, without also immediately deciding how the interaction with derive(Default) is supposed to work, is to have derive(Default) not support field: Type = value syntax at all in the meantime.

There'd also be no way to use the #[derive(Default)] in a way that ignores the = value, even though this syntax's main purpose is only about the meaning of writing Foo { .. }.

For Default this probably isn't a huge problem; who wouldn't want Foo { .. } and Foo::default() to be the same value? And in the odd case where you don't want it, you can always write your own impl Default for Foo.


(From here on, this is not really "just" an answer to @H2CO3 anymore, but brings up new, fairly unrelated points...)

I'm bringing up these points also because they apply to other macros, too. Except none of those have the privilege that Default has: the = value support for Default macro can be introduced together with the default field values feature, so breaking changes can be avoided. The same is not a possibility for other macros. There's two ways to solve the problem of breakage:

  • the macro still requires an attribute on a field if that field's default value in = value syntax should be used for the macro
  • the macro just doesn't support the new = value syntax in a struct/enum it's applied to at all (users will need to update to a new version of the crate providing the macro that does support it)

however, unlike with Default, third-party macro authors don't actually have any way to decide between these two alternatives; either their macro already accepts (and ignores) the new fields, or it doesn't. Only in the latter case can they actually use the = value expression on a field in a meaningful way without also introducing some new syntax for it. (Well, I guess at least in this case they do have the ability to decide something after all.)

Now, many macros use syn, and most macros will not want to care about any new field: Type = value syntax, so it would make a lot of sense if syn just starts accepting this syntax in a non-breaking manner. However, this means that macros that do want to add some way to incorporate the new syntax will fall into the first category above, so they need to add this support it in a non-breaking manner.

serde and structopt both use syn. So probably for serde, something like

#[derive(Deserialize)
struct Foo {
    #[serde(default)
    field: u32 = 42,
}

would still use a default of 0, and you'd need new syntax, for example something like

#[derive(Deserialize)
struct Foo {
    #[serde(default = _)
    field: u32 = 42,
}

to use the provided default value. Arguably, the first case is quite confusing; to avoid confusion, serde should probably (somehow? maybe we'd be getting proc_macro_diagnostics stable befor this feathre..) add a warning for this case, requiring the user to rewrite the thing either to

 #[serde(default = "Default::default()")

or

 #[serde(default = _)

for improved clarity.

Similar considerations apply to structopt, though they'd have (unlike serde) the realistic option of releasing a breaking change in a new major version, so that ... oh wait I just realized that structopt only supports string literals as default values, because those are passed to a clap API that always expects a &str. That means that this feature is completely useless for structopt because the types don't match; unless they'd want to start supporting turning certain literals (like numbers) into string literals (but this still doesn't scale at all to any other more complex types). And in any case, it would be unusable for arg: String fields, which are probably fairly common.


While considering syn, I also noticed that it seems like a pretty hard thing to add these default value declaration to its API in a non-breaking manner.1 Maybe it's even impossible? In which case the discussion above and its conclusions (like that like that serde has to add new syntax such as e.g. serde(default = _)) becomes irrelevant, and OTOH, any worst-case, any macro that doesn't even care about the new = value stuff would presumably need to update to a new 2.0 version of syn?

I'd be interested in hearing what @dtolnay thinks about this.

1The Field: struct doesn't really leave room for the = value part in a struct/enum item. The Foo { .. } expressions seem less problematic as currently, ExprStruct has the already-existing ..rest thing in two separate Option<_>s already. (I don't actually know why it's like that.)

3 Likes

As I mentioned in the zulip thread, this is fairly similar to C++ Default Member Initializers, so I might indicate that as prior art.

But the converse argument could also be made… It could just be a #[derive(Default)] attribute first, and then everyone could continue writing Foo { field1: expr1, ..Foo::default() }. This requires no changes to either the language or the idioms and accomplishes the same result – so then why bother?

1 Like

Something that hasn't been mentioned in the debate so far is that field default currently aren't even syntactically allowed. Eg the following is a compile error:

#[cfg(FALSE)]
struct Foobar {
    foo: i32 = 0
}

Instead I believe it should be like unsafe mod : syntactically valid, but semantically rejected, so it can be used in attribute macros.

Something we could do is only allow the most restrictive case semantically - so only allow const values that match the attribute type - and expect authors who want to more complex patterns to use non-derive attribute macros instead, eg:

#[derive_builder]
struct Lorem {
    ipsum: String,
    // Custom defaults can delegate to helper methods
    // and pass errors to the enclosing `build()` method via `?`.
    dolor: String = self.default_dolor()?,
}

... And, okay, now that I write it out, I realize this example isn't that convenient.

But generally speaking, I think there's something to be said for adding a feature that's as small and forward-compatible as possible, and then expanding from there. If we can cover 90% of use cases with an MVP feature, the remaining 10% will stand out more.

2 Likes

There's another backwards-compatible option for serde here: When you have field: u32 = 42, serde could make field have a serde-default value of 42 when there isn't any serde attribute specified, and require a new attribute like #[serde(no_default)] if you wanted it to not have a default value for serde purposes. (As you say, #[serde(default)] would still have to use u32::default() instead, and should introduce a warning.) Not sure whether this is better, but it seemed worth mentioning.

3 Likes

I implicitly assumed that

#[derive(Default, Deserialize)
struct Foo {
    field: u32 = 42,
}

would always be expected to require field being present for deserialization, as anything else seems quite surprising to me. After all, I'm specifying the = 42 for an easy custom Debug implementation, and Foo { .. } constructor calls; it's not at all clear that I'd want to support new formats for deserialization. (Also, even if what you described was desired behavior, I don't quite see how this makes a difference for backwards-compatibility concerns.)


Edit: I just re-read your answer and only now fully realized the mention of a #[serde(no_default)] attribute. While I disagree for the use-case of serde, and am not convinced that this relates to backwards-compatibility; I do agree that it's worth pointing out that

  • for a third-party derive macro, not necessarily serde::Deserialize though
  • if the goal is to keep the user's ability to choose whether or not a default should be present
  • if it's the common case to activate the "default attribute" whenever possible, and it doesn't seem surprising to make fiel: Type = value automatically enable it
  • then using an opt-out attribute instead of an opt-in attribute is indeed a viable API option

Assuming syn doesn't manage to add in = value support in a non-breaking manner, this way of handling (as well as any other way of handling) = value on fields is even an option that's a non-breaking change. (I.e. under the assumption that currently, the macro would just error when faced with = value syntax.)

This point isn't quite true in the face of private members. With language-level field defaults, private members could be defaulted constructed with { field, .. } syntax, whereas { field, ..default } FRU syntax requires all fields to be public.

Whether mixing public and private fields and providing construction in this manner is a good API choice is a different discussion, but it is enabled by field-level defaulting.

3 Likes

It also requires all fields to have defaults. A huge advantage of the default fields thing is that you can still have some fields without defaults, which are required to be specified in the struct initializer expression.

8 Likes

I think adding a private dummy field with default value would be the correct way of forcing all users of your struct to always construct it with Foo { field_a, field_b, .. } syntax; never with Foo { field_a, field_b }, which in turn has the effect that adding a new public (or private) field with default value to such a struct would be no breaking change. This is - in effect - somewhat similar to #[non_exhaustive] on the struct (but #[non_exhaustive] is equivalent to adding a private field without default value / allows you to add any private fields to a struct without breakage, so it can't even support "Foo { field_a, field_b, .. }"-style construction).

2 Likes

Would it be possible to support different defaults for different macros by supporting some contextual conditional in the expression?

#[derive(Default, Serialize)]
struct Foo {
   x: u32 = if magic_cfg!(deriving == "Serialize") { 1 } else { 2 },
}

My mental model of this is that the default expression would be copy-pasted into different implementations, so each copy would be evaluated in a different context.

I think it's absolutely relevant. Doing that (mixed private/public construction) would be weird, and thus I don't find it the least bit covincing to argue for language-level default values with this sort of requirement.

That's wild. Why would you want to change defaults like that? It seems like a disaster waiting to happen.

4 Likes

I'm not sure if I understand what kind of issue the syntax #[default(...)] / #[default = ...] causes?

In my opinion, it matches the language way better than the proposed syntax.

That's a harsh answer.

I was addressing a question raised earlier in the thread about what if you had a field with a = 1 default, and wanted a different default for some derive macros.

Those would softly imply that this only affects derive(Default), which would be very confusing when it is actually unrelated. The use of field: Type = value does look like let statements, so I wouldn't say that this looks syntactically out of place.

But it can't, even if the feature is implemented as an attribute on the field, this is a new feature enabling something that can't be done today. You can't enforce today that field1 must be user supplied on construction. You can get around this by leveraging the builder pattern.

1 Like

I feel I bit lost. The RFC describes "user-provided default field values". Why would it be unrelated to #[derive(Default)]?

In my opinion, it is actually the best that it looks very related to #[derive(Default)] because it is exactly what it does - configures the output of the derived Default implementation. Other crates could, but don't have to, read this attribute as well.

Sorry, I am assuming familiarity/conflating with the prior discussion at

The idea in that thread evolved to the ability to write

struct Foo {
    mandatory: Type,
    optional: Type = Type::default(),
}
fn main() {
    let _ = Foo { mandatory: Type::foo(), .. };
}

which is something that can't be accomplished with Default because you can't have partially initialized values in Rust. Adding this syntax along with the semantics described in the linked post reduces the need of crates writing builders in their APIs. If such feature is added to the language, then leveraging that new syntax from derive(Default) makes sense in my eyes. If partially defaulted values are never adopted, then instead going for a field attribute that derive(Default) consumes and rejecting new syntax would indeed make more sense.

3 Likes

Thank you so much for this context! It makes a lot more sense now.

To be perfectly honest, I'm still not convinced though. I'm not a fan of the different meanings of Foo { .. } vs Foo { ..Default::default() } and the fact that there are two places where the default values could be set - in the struct definition and in the Default::default implementation. I guess this point has been already raised in the pre-pre-RFC you mentioned.

...but it wouldn't be that hard to learn either :slight_smile: ...and perhaps everybody could agree to consider it bad manners to make them different