-
Feature Name: enum-variant-types
-
Start Date: 2023-6-13
-
RFC PR: rust-lang/rfcs#0000
-
Rust Issue: rust-lang/rust#0000
Summary
Allow enum variants to be their own types, as if they were seperate structs.
This means you can access them without the extra code of an entire match
or if let
.
It would be like "This variable is of type SomeEnum
, but only ever the Foo
variant."
Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
This is important because it allows for easier use of internal data structures
(eg: ASTs, IR/ILs, etc).
The outcome is that enum variants will be able to be destructed as if they were
all seperate structs.
Guide-level explanation
This section assumes familiarity with rust enums and ADTs
Consider the following enum:
enum SomeEnum {
Foo(i32),
Bar(&str),
Baz(u64)
}
Now, lets say you want to use just Foo(i32) for a certain function, but just writing
fn f(x: SomeEnum)
doesnt ensure that unless you add extra code.
This feature allows you to write fn f(x: SomeEnum::Foo)
and if you did:
fn f(x: SomeEnum::Foo) { ... }
f(SomeEnum::Bar("Hello, World!"))
it would error with something along the lines of:
error: function f only takes the variant `Foo`
--> src/main.rs
|
3 | f(SomeEnum::Bar("Hello, World!"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ is variant Bar
|
This means making functions which take only one variant is easier, and as simple as
annotating it in the argument.
Similarly, it can also be done for the returns of functions.
Along with this, you can also use it to annotate variables:
let x: SomeEnum::Foo = SomeEnum::Foo(3);
This allows you to access the fields of SomeEnum::Foo
as if its its own struct.
For example:
println!("{}", x.0); // prints 3
Notice how it's used the same as:
struct Foo(i32);
Finally, they can also be used in generics.
let mut y: Vec<SomeEnum::Foo> = vec![SomeEnum::Foo(3), SomeEnum::Foo(4)];
f(y.pop().unwrap());
You can also lower something of type SomeEnum::Foo
to simply SomeEnum
dynamically. For example:
let x: SomeEnum::Foo = SomeEnum::Foo(5);
let y: SomeEnum = x;
(EVERYTHING UNDER HERE WILL PROBABLY CHANGE) Traits can be implemented to each
variant, and can be used in generics. For example:
impl SomeTrait for SomeEnum::Foo {
...
}
fn somefunc<T>(a: T)
where T: SomeTrait
{
...
}
Doing:
somefunc(SomeEnum::Baz(3));
will result with an error along the lines of:
error[E0277]: the trait bound `SomeEnum: SomeTrait` is not satisfied
--> test.rs:12:14
|
12 | somefunc(SomeEnum::Baz);
| -------- ^^^^^^^^^^^^^ the trait `SomeTrait` is not implemented for `SomeEnum::Baz`
| |
| required by a bound introduced by this call
|
However, doing:
somefunc(SomeEnum::Foo(5));
would work fine as the trait SomeTrait
is implemented for that variant.
Similar to using an unwrap on Option<T>
and Result<T, E>
, you can also
use it on enums to get a specific variant. For example:
// same function as before
let x = some_random_variant();
f(x.unwrap()); // compiler inferrs the variant to be `SomeEnum::Foo`, and expands
// it to .unwrap::<SomeEnum::Foo>()
Notes on usage
The variant will attemt to be inferred through the use of constants of branching:
// constants
let var: SomeEnum = SomeEnum::Foo(32);
f(var); // not an error since var is assigned to a constant `SomeEnum::Foo`
// branching
let var: SomeEnum = some_external_function();
match var {
SomeEnum::Foo(x) => ...
_ => panic!()
}
f(var); // not an error (program exits if its not x)
All traits implemented to the entire enum will be inhereted by each variant.
Reference-level explanation
Implementing this will fit well with normal enums and other code, with no
breaking changes. All uses must be explicitly annotated by the user and doesnt
affect the current uses of enums.
To implement this you would need to first add analysis of the usage of enums,
to infer the variants. Some possible approaches to this can be branching analysis
and constants. Enum variants would also need to become first-class types, and
allowing them to be lowered into just SomeEnum
automatically would need to be
added.
Drawbacks
Why should we not do this?
- Adds complexity to the type system in general
-
Requires extra analysis of prior-code
-
If not done at compile time, it adds overhead to the language
- Analysis can detract from the ergonomics (the compiler cannot know everything
about the program)
- Possible solution:
some_enum.unwrap::<SomeEnum::Variant>()
Rationale and alternatives
This design is the best because currently you have to use pattern matching to
ensure an enum is the right variant, unwrap the values inside, etc, further
making rust easier to write.
It also further enhances readability by making it clear what variants it takes,
and enforces it at compile time.
Alternatives:
- Refined types
Prior art
Unresolved questions
-
#[derive] on different variants?
-
a possible
someenum.unwrap::<SomeEnum::Variant>()
could be added?
Future possibilities
Makes the language more suitable for data analysis, programming language dev,
etc.
Makes enums more ergonomic to use.