-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#[derive(Default)]
on enums with #[default]
- Loading branch information
Showing
1 changed file
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 iff 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<T> { | ||
#[default] | ||
None, | ||
Some(T), | ||
} | ||
``` | ||
|
||
would generate: | ||
|
||
```rust | ||
impl<T> Default for Option<T> { | ||
fn default() -> Self { | ||
Option::None | ||
} | ||
} | ||
``` | ||
|
||
while placing the `#[default]` attribute on `Some(T)` would instead generate: | ||
|
||
```rust | ||
impl<T> Default for Ptr<T> 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<T>` 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<T> { | ||
#[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<T> { | ||
#[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<T>`, 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. |