Default for a subset of fields

Let's say I have a struct with 20 fields, all 19 but 1 fields implemented Default, thus I can't derive Default for my struct. Then When I instantiate my struct, I can't do the following: let x = MyStruct { mandatoryfields: 1, ... Default::default() }

Can we have a special default syntax: AutoDefault? I mean, I don't have to derive Default for my struct. But I can still say:

let x = MyStruct { mandatoryfields: 1, ... AutoDefault() }

Which will supply all the missing fields with its default value. If any missing fields does not implement Default, we got compiler error.

How do you guys think?

3 Likes

Yes, I think something like this has been suggested before, perhaps in concert with Pre-RFC: User-provided default field values. Though I think the syntax should probably be just MyStruct { mandatoryfields: 1, .. } or similar (as there's no AutoDefault() method you could write manually to emulate this.) Also, there was some recent discussion on Zulip regarding how Bevy specifically is struggling with poor ergonomics around defaults.

12 Likes

For what it's worth, I still do intend on revisiting that RFC eventually. It's far from a priority, though.

3 Likes

I was googling this very question and stumbled across this! I'm glad there is an RFC in place! :slight_smile:

I really like @Jules-Bertholet idea for a simple:

MyStruct { mandatoryfields: 1, .. }

Obligatory link to what I'm still hoping we do:

7 Likes

I would say we should boost the priority of this. Not that it's too important, but it's simple, and easy and safe to implement. And helps a lot of beginners. As usually beginners will start wondering why it's so difficult to construct a struct with defaults compare to other languages.

My focus recently has been on an enormous extension to the time crate that I will (hopefully) be able to monetize to an extent. Aside from that, I have an RFC (restrictions) that is accepted and needs its implementation to be reworked, an RFC that is in progress (unsafe fields), and a few other things that I want to work on.

Ultimately, all work I do on Rust and its ecosystem (whether it be RFCs, implementation, discussions, or crate development) is entirely in my free time. It's easy to say "this should be prioritized", but that is simply not where my priorities are at. If my recollection serves me correctly, I had an open offer (over a year ago) to share the bit of the rewrite that I had completed, such that someone else could pick up work on the RFC. No one has reached out for that. So it's not just me — no one is working on it at the moment. That is the nature of volunteer-driven language development. I am happy to discuss something formal to dedicate a certain amount of time towards certain work items, but I imagine this isn't something that you're interested in. Heck, multi-billion dollar corporations have shown their lack of support for development by not doing this. The Foundation can only do so much with limited resources, after all.

8 Likes

I wish Foo { a: bar, ..Defaul::default() } desugared into calling Default on each field, not into calling Default on Foo. It would be so much more useful.

As it stands, Default is actually the only use for Struct update syntax I've ever seen, and it's usability is kneecapped by the need for the entire Struct to implement Default: you usually have a set of fields that have reasonable defaults, and a set of fields that don't. So you either have to break ergonomics by listing all defaults explicitly, or break contract of Default making it return a mostly nonsensical value.

1 Like

good point. I am new to opensource world, don't know exactly how open source world runs. I was always wondering how the hell the whole thing works. Billion dollars business depending on some software built by developers for free? I am surprised it actually worked.

For Rust foundation, I propose an idea: We should have a bid war for RFC got accepted. developers around the world to chip in money for that feature. Then that money goes to RFC author, whoever implement it, testers ... etc. Anyway, details can be worked out. :slight_smile:

It won't be much, maybe a couple thousands dollars per feature can be collected. :slight_smile:

I hope that is a joke.

1 Like

Partial Implementations for Struct would be possible as extension if Partial Types (for Structs) will be added.

#[derive(Default.{b,d})]
struct MyStruct {
    a : non_default_type, 
    b : default_type, 
    c : non_default_type, 
    d : default_type, 
}

let x = MyStruct {a: 5, c : 6, ..<MyStruct.{b,d} as Default>::default()};

I've update a bit syntax

let x = MyStruct {a: 5, c : 6, ..Bikesheed::default()};

could also expend into

let x = MyStruct {
    a: 5,
    c : 6,
    b: Default::default(),
    d: Default::default(),
};

Which doesn't put any requirement on MyStruct.

(but there is the question of what happens if <MyStruct as Default>::default().field differs from <typeof(MyStruct.field) as Default>::default()).

agree!

Sure, but in original proposal was "Let's say I have a struct with 20 fields, all 19 but 1 fields implemented Default"

Second, my approach allows to derive any not-magical trait deriving for partial Struct!

While I have wished for this in the past (and written the impl Default infinite loop with it as well), I do have some questions about how this desugars. If it is syntactic and the X in ..X is copied literally into each non-specified field, what does that mean for ..foo(&mut obj) in this position? Or ..with_side_effects(some_ref)? If it is not syntactic expansion and instead something like { let tmp = foo(&mut obj); Struct { manual: value, dflt: tmp.dflt, other: tmp.other }, what is the type of tmp here?

For the extreme example:

I'd say to just make a constructor that takes the 1 non-Default field's value and use Default::default() on the rest. Sure, a bit verbose in that fn, but it's written once. More "interesting" here is the 10/10 split where the parameters become unwieldy to manage. But such a structure is probably better grouped in…some way. I have a hard time imagining a structure that I wouldn't break down into groups that wasn't repr(C) or at the risk of oodles of padding in any preferable grouping. Are there examples of such structures in the wild that aren't defined in non-Rust code?

If it is syntactic and the X in ..X is copied literally into each non-specified field, what does that mean for ..foo(&mut obj) in this position? Or ..with_side_effects(some_ref) ? If it is not syntactic expansion and instead something like { let tmp = foo(&mut obj); Struct { manual: value, dflt: tmp.dflt, other: tmp.other } , what is the type of tmp here?

This depends what do we want from partial type. ..foo(&mut obj) could be

  • either ..foo(&mut obj.{b,d}) this is a partial borrow (if function foo allows this)
  • or ..foo(&mut obj).{b,d} we cut type after foo-calculation
  • or something more complex like ..foo(&mut obj.{a,b,d}).{b,d}

Which is essentially how functional record updates work today: you evaluate the expression normally and then move each needed field into the newly created struct.

I'm empathetic to the argument that FRU semantics are weird, because it'd be more approachable why it's like this if it were a matter of Vec3 { x: 0, ..default() } being { let mut _tmp: Vec3 = default(); _tmp.x = 0; _tmp } instead of what it's closer[1] to, { let _tmp: Vec3 = default(); Vec3 { x: 0, y: _tmp.y, z: _tmp.z }. As it is, you neither get the full benefit of wholesale default initialization (being able to default private fields and customize others) or of piecewise initialization (not needing to compute a default value for the ones you're directly specifying).[2]

With the few people I've helped advise, I've found some good results from explaining FRU as more like sugar for setting the mentioned fields on the default object (with a restriction that all fields must be visible) than it is like sugar for assigning the unmentioned fields in the record literal. The latter is more accurate, but the difference rarely comes up, and relies on the difference between a value (C++ rvalue) and a place (C++ lvalue) to truly matter, which is an advanced concept.

Type adjusting FRU (RFC accepted but not yet implemented IIRC) gives FRU a bit more applicability and reason for being this easy, but partially defaulted construction is still by and far the most common. I do look forward to when fieldwise defaults happen, but unfortunately just don't have the time/energy to try to push it through, especially when I have multi-year-old "pet" (lib) features already on nightly I should be pushing for....


  1. If you know exactly how it's different from this desugar before I allude to it in the next paragraph, you win today's bonus cookie. ↩︎

  2. What you do get is only rarely applicable in practice: you can do FRU from an unowned place (i.e. from behind a reference) if all the needed fields are Copy, even if some of the other fields are not. ↩︎

2 Likes

I didn't realize we were talking about partial types here as well. obj need not be anything related to Struct but could be StructParams.

We ideally would want both to coexist. I actually use the update syntax with existing structs quite often when I'm messing with data structures and smart pointers, but I digress.

In order for what you're asking to be implemented kind of coherently, we'd need polymorphic closures and trait generics, but we have neither of those. However, I think we could work around that by just hacking in polymorphic closures here (copy-pasting it on each field generation before type-checking), and then type checking the signature of fn<T: GenericTrait>() -> T for every field. We just need a way to somehow express that GenericTrait, which I can't really come up with right now, it's very disruptive in the language.

So... something like this?

struct Foo {
    a: i32,
    b: i32,
    c: i32,
    d: i32,
}

struct FooParams {
    a: i32,
    d: i32,
}

// Magic marker trait
impl PartialUnpack<Foo> for FooParams {}

trait PartialDefault {
    type Partial: PartialUnpack<Self>;

    fn partial_default() -> Self::Partial;
}

impl PartialDefault for Foo {
    type Partial = FooParams;

    fn partial_default() -> Self::Partial {
        FooParams {
            a: 100,
            d: 200,
        }
    }
}

fn main() {
    let foo = Foo {
        b: 300,
        c: 400,
        // This is allowed because of the PartialUnpack
        ..Foo::partial_default()
    }
}

Sure. Things I see:

  • Are we OK with aligning based on (private) field names here?
  • What semver problems exist as this seems possible (e.g., can I opt-out of this support?):
impl PartialUnpack<ForeignFoo> for LocalParams {}
  • Where does type checking occur? At use time? I feel like the Rust-y answer would be at fn partial_default().