Pre-RFC: User-provided default field values

I don't see why we should typecheck it at all. It can become a valid syntax with no meaning, that is open for macro use (though I agree with @steffahn that it's better if we'll require macro authors to specify a dependency on it).

But I have other problem with this proposal: the syntax can be used only once. If you use it for #[derive(Default)], for example, you can't use it for anything else, say #[serde(default)].

And it means one of two:

  • You fall back to existing mechanisms like attributes.
  • You don't need different expressions, because they should be the same. But then there isn't much of advantage in this RFC, since you can just express all of them by a manually-implemented Default, and manually implementing one trait is not such a big deal.

It took me a minute to realize that this is not the case as well. No, it's not the same, because a derive has to be explicitly included whereas Centril's RFC is native syntax that's always present.

It's semantically a default value for a field of a given type. Isn't that reason enough?


It will make it possible to use them for Centril's RFC, but it's only a symptom: the general problem, that we cannot change a feature to support default values - only until we stabilize or new features, will stay.


And the context varies. When #[derive(Default)] will paste it in the body of fn default() -> Self it will be typechecked there. But there can be other contexts. For example, what if some framework will allow you to provide async/fallible default?

I don't understand your question. Suppose I want to provide default value of 1 to the Default trait and 2 for serde. I can't.

Serde could still have an attribute for that special case, while using the common syntax for the easy case where all default-using derives need the same value. (Serde would still need the attribute for backward compatibility anyway, they simply need to pick the correct priority rule.)

That said, starting to support the common syntax might be a breaking change for a crate, since it could be present for another derive and subtly change behavior.

#[derive(changing_crate::WasUsingDefaultTrait, other_crate::AlreadyUsingSyntax)]
struct X {
    x: u8 = 1;

Whoops, WasUsingDefaultTrait now uses 1 instead of 0. (The above case was misleading before the change in changing_crate, but I believe it is not considered to be important for the definition of “breaking change”?)

I explicitly pointed on that:

This was already said:

cc @ekuber , since I'd still like to see exactly what was worked out in their previous thread


I don't see what the value of this is. Why is there need for custom syntax if attributes can achieve the same thing?

AFAIK non-string attributes are already stable, so the author of structopt could just update the crate so that it accepts non-string expressions for defaults as well, and the implementation of #[derive(Default)] could do the same. There is absolutely zero justification for separate syntax here.


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.


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

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

struct Foo {
    field: u32 = 42,

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

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()")


 #[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 thing in two separate Option<_>s already. (I don't actually know why it's like that.)


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:

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:

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.


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.


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.


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.


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).


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.