Struct field defaults

Just testing the waters for now, how interested would people be in having default values for struct fields?

Something like

struct Foo {
    a: String,
    b: i32 = 42,
}

Then when instantiating:

let f = Foo { a: "Hello".to_owned() };  // b gets default value of 42
let f = Foo { a: "Hello".to_owned(), .. };  // alternative syntax to make default use explicit
let f = Foo { a: "Hello".to_owned(), b: 51 };  // b is 'overridden'

I have sometimes wanted this, but the alternative of having an empty ctor (or implementing Default) and then using .. Foo::new() is not too painful. On the other hand, it is a small and self-contained feature, so the cost of adding it is low.

What do others think?

21 Likes

I use this feature often in D language. It’s often enough handy and I think it doesn’t collide with other things.

Would this take constants only? Or could it allow calling some Fn + 'static? I could especially see wanting to use the field type’s Default here.

dunno. I haven’t thought this through in detail, just seeing if there is enough interest to make it worth thinking through

I feel to allow

let f = Foo { a: "Hello".to_owned(), .. };

for b that implements Default is less surprising to readers of the code.

I don’t have concrete reason to oppose the defaults in general. Just feel that if we disallow function reload and function argument defaults, we should also disallow field defaults.

1 Like

I agree with the “small, self-contained” aspect. This feels like the sort of feature that’s isolated enough that it’s really just a question of “what are the exact semantics?” and “is the implementation effort commensurate with the benefit?”.

Actually, another benefit: this, along with privacy, gives library writers a way to create structs you can’t exhaustively match, but still explicitly construct. i.e. let’s say you have:

struct Whatsis {
    pub thingy: i32,
    pub hoozits: &'static str,
    inexhaustive: () = ()
}

This means you can let users construct as Whatsis { thingy: 42, hoozits: "blammo", .. } rather than the equivalent (but more opaque) Whatsis::new(42, hoozits), and still being able to add fields in the future (provided they can be defaulted).

3 Likes

I feel this conflicts with the Default trait. What does this mean:

struct Foo {
    a: i32 = 10,
}

impl Default for Foo {
    fn default() -> Foo {
        Foo { a: 6 }
   }
}

Alternatively, what if you derive Default?

3 Likes

Presumably, because “field defaults” don’t have to be provided for every field, they’re not the same thing as a Default implementation. Thus, they technically wouldn’t overlap.

#[derive(Default)] could be modified to use field defaults where present, and fall back to Default otherwise.

7 Likes

What's the rationale behind not having function argument defaults?

Unlike struct literals, function invocation doesn’t have a syntax for “skipping” arguments. Which is to say: it’d be kinda sucky to have defaultable arguments but not named argument syntax. I suppose you could use _ but that’s icky: foo(_, _, "dingbat", _, Cheeseburger, _, _, _, -8) (pedanting about functions with too many arguments aside).

2 Likes

I would say it’s explicitness.

Every design decision needs to consider its costs and balance them. Not having default arguments and function overloads forces your API to be larger, because you need more functions with a different number of arguments. And this sometimes causes problems:

In that case format() is "taken" because you can't change the function later to make the second argument optional. So the API is worse, you have a longer named function (format_default) to replace a simpler function (the one with just one argument).

I can say the same about C++.
I'd also appreciate default function arguments.

I am sure there are use cases where these features will help you type fewer keys when you first code your program. Whether they are justifiable to be added is another issue. Apart from the potential feature bloat problem, having the default spelled out usually helps in the domain Rust is supposed to be used in. These domains include OS/drivers, infrastructures, game engines. Unlike web programming or scripts, code in these domain tend to be read a lot more than be written. Code is also less likely to change in these areas, thus typed again. I don’t seen much benefit in saving a few keytypes. Overloads, ad-hoc defaults (not Default trait) add mental load to code readers and maintainers, and sometimes become source of bug. I have seen a lot of surprising, implicit defaults caused bugs introduced in our production code in C++. Various guidelines are formed just to forbid these kind of features in C++, and I think this is not uncommon in the industry. Coming from C++, I certainly hope Rust would not have the same number of features C++ has. I worry that even if we just slowly add features like this Rust will have just too many features even before becoming mainstream like C++.

1 Like

What guidelines say that default values for struct fiends are bad?

Internal guidelines. Similarly, config files are required to spell out values explicitly unless there are really natural, unsurprising defaults. Apology if this seems away from topic.

I’m not against default values in fields completely, just thinking it should be limited. I am perfectly fine with using .. to indicate fields that should take the value of Default::default(), since when we implement Default for a type, the value is usually natural and does not surprise anyone, such as 0 for u32, None for Option<Value>. But using .. to denote an i32 takes -1 worries me, and I would like to have it explicitly spelled out even at the call site in most cases so that reader would not get a false impression. If this default value is indeed appreciated, then maybe an explicit constructor that takes only other fields, or using associate constants can help.

I would use this all the time.

1 Like

Although already noted elsewhere, I want to bring up here too, that the Default trait builds and returns a whole struct and if another struct’s default values are taken from that, you might need some compiler optimization to not do any needless work (to not initialize and drop fields that are not even used in the resulting struct). I’m not even sure if optimizing everything away is possible in the general case, because calling default() might have side effects.

I’d appreciate if someone with greater knowledge would chime in and verify if this is a thing that affects performance, or am I worrying for nothing?

That’s why the rule in C++ requires that all the defaulted function parameters will be after the non-defaulted ones.

I wonder why we don’t have so obvious thing from the beginning :slight_smile:

2 Likes