From bae1c39d6b367c25f853c58c24f1c3439cae3e23 Mon Sep 17 00:00:00 2001 From: Jacob Pratt Date: Sun, 11 Apr 2021 23:33:40 -0400 Subject: [PATCH] `#[derive(Default)]` on enums with `#[default]` --- text/0000-derive-enum-default.md | 363 +++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 text/0000-derive-enum-default.md diff --git a/text/0000-derive-enum-default.md b/text/0000-derive-enum-default.md new file mode 100644 index 00000000000..80c62e73393 --- /dev/null +++ b/text/0000-derive-enum-default.md @@ -0,0 +1,363 @@ +- Feature Name: `derive_enum_default` +- Start Date: 2021-04-07 +- RFC PR: TODO +- Rust Issue: TODO + +# Summary +[summary]: #summary + +An attribute `#[default]`, usable on `enum` variants, is also introduced, thereby allowing enums to +work with `#[derive(Default)]`. + +```rust +#[derive(Default)] +enum Foo { + #[default] + Alpha(u8), + Beta, + Gamma, +} + +assert_eq!(Foo::default(), Foo::Alpha(0)); +``` + +The `#[default]` attribute may not be used on a variant that is also declared `#[non_exhaustive]`. + +# Motivation +[motivation]: #motivation + +## `#[derive(Default)]` in more cases + +Currently, `#[derive(Default)]` is not usable for `enum`s. To rectify this situation, a `#[default]` +attribute is introduced that can be attached to variants. This allows you to use +`#[derive(Default)]` on enums wherefore you can now write: + +```rust +// from time +#[derive(Default)] +enum Padding { + Space, + Zero, + #[default] + None, +} +``` + +## Clearer documentation and more local reasoning + +Providing good defaults when such exist is part of any good design that makes a physical tool, UI +design, or even data-type more ergonomic and easily usable. However, that does not mean that the +defaults provided can just be ignored and that they need not be understood. This is especially the +case when you are moving away from said defaults and need to understand what they were. Furthermore, +it is not too uncommon to see authors writing in the documentation of a data-type that a certain +value is the default. + +All in all, the defaults of a data-type are therefore important properties. By encoding the defaults +right where the data-type is defined gains can be made in terms of readability particularly with +regard to. the ease of skimming through code. In particular, it is easier to see what the default +variant is if you can directly look at the `rustdoc` page and read: + +```rust +#[derive(Default)] +enum Foo { + #[default] + Bar { + alpha: u8, + }, + Baz { + beta: u16, + gamma: bool, + } +} +``` + +This way, you do not need to open up the code of the `Default` implementation to see what the +default variant is. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +The ability to add default values to fields of `enum` variants does not mean that you can suddenly +`#[derive(Default)]` on the enum. A Rust compiler will still have no idea which variant you intended +as the default. This RFC adds the ability to mark one variant with `#[default]`: + +```rust +#[derive(Default)] +enum Ingredient { + Tomato, + Onion, + #[default] + Lettuce, +} +``` + +Now the compiler knows that `Ingredient::Lettuce` should be considered the default and will +accordingly generate an appropriate implementation of `Default for Ingredient`: + +```rust +impl Default for Ingredient { + fn default() -> Self { + Ingredient::Lettuce + } +} +``` + +Note that after any `cfg`-stripping has occurred, it is an error to have `#[default]` specified on +more than one variant. + +Due to the potential of generated bounds becoming more restrictive with an additional field, the +`#[default]` and `#[non_exhaustive]` attributes may not be placed on the same variant. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## `#[default]` on `enum`s + +A built-in attribute `#[default]` is provided the compiler and may be legally placed solely on +exhaustive `enum` variants. The attribute has no semantics on its own. Placing the attribute on +anything else will result in a compilation error. Furthermore, if the attribute occurs on more than +one variant of the same `enum` data-type after `cfg`-stripping and macro expansion is done, this +will also result in a compilation error. + +## `#[derive(Default)]` + +Placing `#[derive(Default)]` on an `enum` named `$e` is permissible if and only if that enum has +some variant `$v` with `#[default]` on it. In that event, the compiler shall generate an +implementation of `Default` where the function `default` is defined as (where `$f_i` denotes a +vector of the fields of `$e::$v`): + +```rust +fn default() -> Self { + $e::$v { $f_i: Default::default() } +} +``` + +### Generated bounds + +To avoid needlessly strict bounds, all types present in the tagged variant's fields shall be bound +by `Default` in the generated code. + +```rust +#[derive(Default)] +enum Option { + #[default] + None, + Some(T), +} +``` + +would generate: + +```rust +impl Default for Option { + fn default() -> Self { + Option::None + } +} +``` + +while placing the `#[default]` attribute on `Some(T)` would instead generate: + +```rust +impl Default for Ptr where T: Default { + fn default() -> Self { + Option::Some(Default::default()) + } +} +``` + +## Interaction with `#[non_exhaustive]` + +The Rust compiler shall not permit `#[default]` and `#[non_exhaustive]` to be present on the same +variant. Any variant not designated `#[default]` may be `#[non_exhaustive]`, as can the `enum` +itself. + +# Drawbacks +[drawbacks]: #drawbacks + +The usual drawback of increasing the complexity of the language applies. However, the degree to +which complexity is increased is not substantial. One notable change is the addition of an attribute +for a built-in `#[derive]`, which has no precedent. + +# Rationale +[rationale]: #rationale + +The inability to derive `Default` on `enum`s has been noted on a number of occasions, with a common +suggestion being to add a `#[default]` attribute (or similar) as this RFC proposes. + +- [IRLO] [Request: derive enum's default][rationale-1] +- [IRLO] [Deriving `Error` (comment)][rationale-2] +- [URLO] [Crate for macro for default enum variant][rationale-3] +- [URLO] [`#[derive(Default)]` for enum, [not] only struct][rationale-4] + +[rationale-1]: https://internals.rust-lang.org/t/request-derive-enums-default/10576?u=jhpratt +[rationale-2]: https://internals.rust-lang.org/t/deriving-error/11894/10?u=jhpratt +[rationale-3]: https://users.rust-lang.org/t/crate-for-macro-for-default-enum-variant/44032?u=jhpratt +[rationale-4]: https://users.rust-lang.org/t/derive-default-for-enum-non-only-struct/44046?u=jhpratt + +Bounds being generated based on the tagged variant is necessary to avoid overly strict bounds. If +this were not the case, the previous example of `Option` would require `T: Default` even though +it is unnecessary because `Option::None` does not use `T`. + +Prohibiting `#[non_exhaustive]` variants from being tagged with `#[default]` is necessary to avoid +the possibility of a breaking change when additional fields are added. If this were not the case, +the following could occur: + +A definition of + +```rust +#[derive(Default)] +enum Foo { + #[default] + #[non_exhaustive] + Alpha, + Beta(T), +} +``` + +which would not have any required bounds on the generated code. If this were changed to + +```rust +#[derive(Default)] +enum Foo { + #[default] + #[non_exhaustive] + Alpha(T), + Beta(T), +} +``` + +then any code where `T: !Default` would now fail to compile. + +# Alternatives +[alternatives]: #alternatives + +One alternative is to permit the user to declare the default variant in the derive itself, such as +`#[derive(Default(VariantName))]`. This has the disadvantage that the variant name is present in +multiple locations in the declaration, increasing the likelihood of a typo (and thus an error). + +Another alternative is assigning the first variant to be default when `#[derive(Default)]` is +present. This may prevent a `#[derive(PartialOrd)]` on some `enum`s where order is important (unless +the user were to explicitly assign the discriminant). + +# Prior art +[prior-art]: #prior-art + +## Procedural macros + +There are a number of crates which to varying degrees afford macros for default field values and +associated facilities. + +### `#[derive(Derivative)]` + +[`derivative`]: https://crates.io/crates/derivative + +The crate [`derivative`] provides the `#[derivative(Default)]` attribute. With it, you may write: + +```rust +#[derive(Derivative)] +#[derivative(Default)] +enum Foo { + #[derivative(Default)] + Bar, + Baz, +} +``` + +Contrast this with the equivalent in the style of this RFC: + +```rust +#[derive(Default)] +enum Foo { + #[default] + Bar, + Baz, +} +``` + +Like in this RFC, `derivative` allows you to derive `Default` for `enum`s. The syntax used in the +macro is `#[derivative(Default)]` whereas the RFC provides the more ergonomic and direct notation +`#[default]` in this RFC. + +### `#[derive(SmartDefault)]` + +[`smart-default`]: https://crates.io/crates/smart-default + +The [`smart-default`] provides `#[derive(SmartDefault)]` custom derive macro. It functions similarly +to `derivative` but is specialized for the `Default` trait. With it, you can write: + +```rust +#[derive(SmartDefault)] +enum Foo { + #[default] + Bar, + Baz, +} +``` + +- The same syntax `#[default]` is used both by `smart-default` and by this RFC. While it may seem + that this RFC was inspired by `smart-default`, this is not the case. Rather, this notation has + been independently thought of on multiple occasions. That suggests that the notation is intuitive + since and a solid design choice. + +- There is no trait `SmartDefault` even though it is being derived. This works because + `#[proc_macro_derive(SmartDefault)]` is in fact not tied to any trait. That `#[derive(Serialize)]` + refers to the same trait as the name of the macro is from the perspective of the language's static + semantics entirely coincidental. + + However, for users who aren't aware of this, it may seem strange that `SmartDefault` should derive + for the `Default` trait. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- [x] Should the generated bounds be those required by the tagged variant or those of the union of + all variants? This matters for `enums` similar to `Option`, where the default is `Option::None` + — a value that does not require `T: Default`. + + _Resolved_ in favor of requiring all types in the only the tagged variant to be bound by + `Default`. + +# Future possibilities +[future-possibilities]: #future-possibilities + +The `#[default]` attribute could be extended to override otherwise derived default values, such as + +```rust +#[derive(Default)] +struct Foo { + alpha: u8, + #[default = 1] + beta: u8, +} +``` + +which would result in + +```rust +impl Default for Foo { + fn default() -> Self { + Foo { + alpha: Default::default(), + beta: 1, + } + } +} +``` + +being generated. + +Alternatively, dedicated syntax could be provided [as proposed by @Centril][centril-rfc]: + +[centril-rfc]: https://github.com/Centril/rfcs/pull/19 + +```rust +#[derive(Default)] +struct Foo { + alpha: u8, + beta: u8 = 1, +} +``` + +If consensus can be reached on desired bounds, there should be no technical restrictions on +permitting the `#[default]` attribute on a `#[non_exhaustive]` variant.