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

I have interacted with some verbose types that are designed to leverage a Default implementation heavily, foregoing the builder pattern. This can look incredibly verbose:

Foo {
  bar: [Bar {
    qux: [Qux {
      zap: [Zap {
         val: "val",
         ..Default::default()
      }],
      ..Default::default()
    }],
    ..Default::default()
  }],
  ..Default::default()
}

I would like to propose the introduction of some syntactic sugar, not dissimilar to ?, for Default::default(). There are multiple alternatives for what this could be, but in my mind it could be the sequence ..* (which shouldn't introduce ambiguity in the grammar, even though it would introduce some ambiguity when teaching the language):

Foo {
  bar: [Bar {
    qux: [Qux {
      zap: [Zap {
         val: "val",
         ..*
      }],
      ..*
    }],
    ..*
  }],
  ..*
}

Another alternative would be to make default a contextual keyword (Foo { ..default }) or using another sigil or keyword.

Have people encountered a need for this? I believe that introducing this in isolation might not be worth it, unless other features complement it. In my mind, if anonymous structs in fn argument position are also incorporated in the language, then that would be in effect "named arguments" and having this sugar will also help use these two features as a way of getting "optional fn arguments":

fn foo(_ { bar: Option<usize>, baz: Option<usize> }) {}
fn main() {
    foo(_ { bar: Some(3), ..* });
}

What do people think?

4 Likes

I’ve frequently desired default as a contextual keyword for this exact scenario, but haven’t ever raised it in a venue for discussion.

7 Likes

I'd prefer just ".." instead of "..*": shorter and no learning issue of wondering what "*" means.

18 Likes

I was concerned about the potential to confuse people with the pattern position Foo { .. } and the expression Foo { .. }, as well as the short distance between a typo (forgetting to add a specific already constructed binding) and "valid" code, but it is possible it wouldn't be too bad in either case.

3 Likes

Comparing to patterns, .. seems like it has the appropriate duality to me.

5 Likes

If I understand correctly, default as a contextual keyword in place of * here would not work, because Foo { ..default } is already valid Rust code.

7 Likes

This reminds me of the conversation in Short enum variant syntax in some cases, where I tossed together this macro:

macro_rules! s {
    ( $($i:ident : $e:expr),* ) => {
        {
            let mut temp = Default::default();
            if false {
                temp // this tricks inference into working
            } else {
                $(
                    temp.$i = $e;
                )*
                temp
            }
        }
    }
}

With that, I think your example would be

let _: Foo = s! {
  bar: [s! {
    qux: [s! {
      zap: [s! {
         val: "val",
      }],
    }],
  }],
}

Having some sort of language feature (or even just a macro) like that could be cool, since it sounds like your situation is also one where it'd be nice to not have to type the name of the type.

It also makes me once again want the different version of FRU that can work with non_exhaustive types or those with private fields, so more things can use this kind of API if they want.

7 Likes

You could already simplify this to

use Default::default;

Foo {
  bar: [Bar {
    qux: [Qux {
      zap: [Zap {
         val: "val",
         ..default()
      }],
      ..default()
    }],
    ..default()
  }],
  ..default()
}

which is quite close to your proposed default keyword.

Edit: Actually I just tested this and it turns out that items in traits are not importable.

2 Likes

You can accomplish the same shortening by writing:

fn default<T: Default>() -> T {Default::default()}
7 Likes

I’ve noticed the calls for "syntactic sugar" or need for macros in Rust just because of its wordiness / explicitness. Default::default() is one example. Another one is all those helper macros for implementing traits in crates like derive_more or smart_default. Coming from Haskell I mostly see artificial and unnecessary problems here.

In Haskell, functions from type classes (i.e. traits) are never namespaced under the name of the typeclass. So when importing the Default class, you can immediately just write def where a Rustacean needs to write Default::default(). Thats 3 vs 18 characters. The artificial problem here is that what is unavoidable in Haskell (non-namespaced functions from traits) is impossible in Rust, without resorting to writing a new wrapper function.

At least the wrapper function is possible so you can

fn def<T: Default>() -> T {Default::default()}

and your problem goes away

Foo {
  bar: [Bar {
    qux: [Qux {
      zap: [Zap {
         val: "val",
         ..def()
      }],
      ..def()
    }],
    ..def()
  }],
  ..def()
}

I would be quite happy, if the wraper function could be replaced by a

use Default::default as def;

Also having Default::default pre-imported (as default) in the prelude is kind-of a must.


I won’t go too much into my off-topic point about implementing traits but I see a big problem in this need of re-stating the entire function/method signature of every function/method in every trait impl. I don’t think these helper macro crates were half as popular if implementing traits was more fun and less boilerplate. (And, by the way, I don’t think that the explicit signatures are the only problem that makes implementing traits tedious. There’s lots of room for improvement, including things Haskell doesn’t have (yet) either.)

7 Likes

..* would be ambiguous with ..*some_ptr_or_ref.

Nightly currently has a free function default, so you could just write ..default() rather than ..Default::default(). That would be a substantial improvement, without adding syntax.

19 Likes

Interesting! Just the other day I was thinking that struct update syntax is a feature that I use or encounter in code I read exceedingly rare, and was wondering whether this feature pulls its weight at all...

Having a free function default() in the prelude would be great. I currently use <_>::default() fairly often to avoid imports and type repetition.

3 Likes

I think a huge part of this is that it only works on types where all the fields are accessible (all public and not non-exhaustive, or inside the same crate). That basically keeps libraries from using it for "options structs" unless they're willing to do a major version bump for every field addition. Thus they're forced into more-boilerplate builder APIs even if they don't need the extra capabilities those can offer.

I regularly use C# APIs that have a ton of these (random example) and Rust could do an even better job at this, thanks to inference and being able to pass them as &FooOptions. (In C# with shared mutable ownership it's never clear who might be modifying something, especially in convenience wrappers.)

It also breaks in less-obvious cases, like types that implement Drop and have non-Copy fields. Personally, rough edges like this make me less likely to use update syntax as part of my habitual Rust toolbox.

2 Likes

I'm currently using a library where all the methods look like async fn do_a_thing(&self, req: DoAThingRequest) -> ..., so all my calls to it look like thing_doer.do_a_thing(DoAThing { field, field2, ..Default::default() }).compat().await?.

That's a large amount of boilerplate. default() will help. I also hope I can use _ { field, field2, ..default() } someday.

1 Like

Some of the concerns raised seem to be around the minor sharp edges around the existing feature, which I understand. Another one that hasn't been raised is it is impossible to have partial applicability: having a struct where some fields are optional with default values, but some that are mandatory. Because of the way the feature works it wouldn't be possible today to write something like Foo { mandatory_field, ..default() } because Default can't really be implemented for it.

But these are in my eyes current limitations that we could work around somehow, and if we did we could provide a very idiomatic and less verbose alternative to the builder pattern.

3 Likes

I'm in favor of having a Struct { field: value, .. } syntax. But I think it would be better served by being a first-class "default the rest of the fields" feature rather than just sugar for standard FRU, so that it's usable for more cases.

6 Likes

That reminds me of yet another previous conversation about allowing customized field defaults. One could imagine struct Foo { x: i32 = 4 } such that Foo { .. } gave Foo { x: 4 }. So we have to be careful when picking exactly which semantic is desired here.

EDIT: That again also gets to non_exhaustive too -- one would want a way to say "this is non_exhaustive but I promise I'll only add things with defaults" or something.

6 Likes

If all the fields in the struct are Default or have a default value, you could easily write a proc_macro to provide that feature. Adding it to the language could have the benefit of giving the compiler enough context for the "some fields have defaults and some not so you have a set of mandatory fields for construction" case.

I think that what I would like to see is the composition of this feature, the proposed feature and anonymous literals/structural records to allow us to write foo(_ { x = 1, .. }) when given

fn foo(_ { x: i32, y: i32 = 42 })

or

struct Bar {
    x: i32,
    y: i32 = 42,
}
fn foo(Bar { x, y }: Bar) {}

But of course, the individual features should stand on their own merits.