Allow constructing non_exhaustive structs using ..Default::default()

Let's say there is a config struct that decides some behavior in some library:

struct Config {
    pub use_foo: bool,
    pub far_enabled: bool,
    pub title: String,
}

Right now, adding any new configuration options is a breaking change. This is undesirable because I may want to expand the library's functionality in the future.

At first, I considered using #[non_exhaustive], because that's supposed to signal to the user (and the compiler) that more fields may be added in the future. If my Config implemented Default, I assumed that meant that downstream users could just use this syntax:

let config = Config {
    use_foo: false,
    ..Default::default()
}

That way, any new fields that are added will be given their default values. Seems perfectly reasonable, right? Well, that's forbidden by the compiler:

error[E0639]: cannot create non-exhaustive struct using struct expression

Looking up that error code, it looks like #[non_exhaustive] is only meant to signal that more fields may be added in the future - the exact behavior to enforce that does not seem exactly defined.

Why am I not allowed to construct using an existing instance which already has all the fields? That would allow users to specify the config options they want to change while new fields will simply be set to their defaults.

I think #[non_exhaustive] should allow you to use struct expressions if you also use ..<some_instance_of_the_struct>. That way, the compiler can guarantee that you will have all the fields, because any fields you miss in your expression can be filled in by the fields of that <some_instance_of_the_struct>. Default::default() fills this role perfectly and would be useful for configuration.

Thoughts?

Please note that the next best alternative is the BuilderBuilderBuilder pattern and I don't want to force that on my users for any reason.

let config_builder_builder: ConfigBuilderBuilder<BuilderVersionOne> = ConfigBuilderBuilderBuilder::new()
    .use_builder_builder_pattern_version(BuilderBuilderPatternVersion::One)
    .builder_builder();

let config_builder = config_builder_builder
    .add_support_for_option(ConfigOption::InvertY) // ConfigOption is non_exhaustive
    .builder();

let config = config_builder
    .set_config_option(ConfigOption::InvertY) // not specifying the option panics
    // specifying unsupported options panics
    .build_config();

// now you have a config
3 Likes

You'll probably be interested in this thread, and upcoming RFC, from ekuber:

3 Likes

I think that's cool :smiley: I hope that it can work with #[non_exhaustive] like Default::default() does in my example.

RFC coming shortly!

This is not an edge I had thought about too much. It feels like it could be an independent (smaller) RFC for which I see no backwards compat issues as long as the usage of non_exhaustive in the wild conforms to its spirit. I would be for this change (and shouldn't be too hard to do), but needs to be approved by [T-lang].

4 Likes

I've seen it advertised as a quick hack to force people to go through your constructor function, even if all fields are public. I don't think they would provide a Default impl in that case, though.

2 Likes

Because of the way FRU desugars it can't access private fields, and non_exhaustive doesn't say that you'll only add public fields in the future.

Some past history: Pre-RFC: Relaxed #[non_exhaustive] structs - #15 by scottmcm

4 Likes

Ah, yes. I have encountered this before.

I was going to link to the post, but it turns out it's the same post you just linked.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.