Description
Proposal
Summary and problem statement
(taken from: rust-lang/rfcs#2593)
Consider enum variants types in their own rights. This allows them to be irrefutably matched
upon. Where possible, type inference will infer variant types, but as variant types may always be
treated as enum types this does not cause any issues with backwards-compatibility.
enum Either<A, B> { L(A), R(B) }
fn all_right<A, B>(b: B) -> Either<A, B>::R {
Either::R(b)
}
let Either::R(b) = all_right::<(), _>(1729);
println!("b = {}", b);
Motivation, use-cases, and solution sketches
When working with enums, it is frequently the case that some branches of code have assurance that
they are handling a particular variant of the enum (1, 2, 3, 4, 5, etc.). This is especially the case when abstracting
behaviour for a certain enum variant. However, currently, this information is entirely hidden to the
compiler and so the enum types must be matched upon even when the variant is certainly known.
By treating enum variants as types in their own right, this kind of abstraction is made cleaner,
avoiding the need for code patterns such as:
- Passing a known variant to a function, matching on it, and use
unreachable!()
arms for the other
variants. - Passing individual fields from the variant to a function.
- Duplicating a variant as a standalone
struct
.
However, though abstracting behaviour for specific variants is often convenient, it is understood
that such variants are intended to be treated as enums in general. As such, the variant types
proposed here have identical representations to their enums; the extra type information is simply
used for type checking and permitting irrefutable matches on enum variants.
Guide-level explanation
The variants of an enum are considered types in their own right, though they are necessarily
more restricted than most user-defined types. This means that when you define an enum, you are more
precisely defining a collection of types: the enumeration itself, as well as each of its
variants. However, the variant types act identically to the enum type in the majority of cases.
Specifically, variant types act differently to enum types in the following case:
- When pattern-matching on a variant type, only the constructor corresponding to the variant is
considered possible. Therefore you may irrefutably pattern-match on a variant:
enum Sum { A(u32), B, C }
fn print_A(a: Sum::A) {
let A(x) = a;
println!("a is {}", x);
}
However, to be backwards-compatible with existing handling of variants as enums, matches on
variant types will permit (and simply ignore) arms that correspond to other variants:
let a = Sum::A(20);
match a {
A(x) => println!("a is {}", x),
B => println!("a is B"), // ok, but unreachable
C => println!("a is C"), // ok, but unreachable
}
To avoid this behaviour, a new lint, strict_variant_matching
will be added that will forbid
matching on other variants.
- You may project the fields of a variant type, similarly to tuples or structs:
fn print_A(a: Sum::A) {
println!("a is {}", a.0);
}
Variant types, unlike most user-defined types are subject to the following restriction:
- Variant types may not have inherent impls, or implemented traits. That means
impl Enum::Variant
andimpl Trait for Enum::Variant
are forbidden. This dissuades inclinations to implement
abstraction using behaviour-switching on enums (for example, by simulating inheritance-based
subtyping, with the enum type as the parent and each variant as children), rather than using traits
as is natural in Rust.
enum Sum { A(u32), B, C }
impl Sum::A { // ERROR: variant types may not have specific implementations
// ...
}
error[E0XXX]: variant types may not have specific implementations
--> src/lib.rs:3:6
|
3 | impl Sum::A {
| ^^^^^^
| |
| `Sum::A` is a variant type
| help: you can try using the variant's enum: `Sum`
Variant types may be aliased with type aliases:
enum Sum { A(u32), B, C }
type SumA = Sum::A;
// `SumA` may now be used identically to `Sum::A`.
If a value of a variant type is explicitly coerced or cast to the type of its enum using a type
annotation, as
, or by passing it as an argument or return-value to or from a function, the variant
information is lost (that is, a variant type is different to an enum type, even though they behave
similarly).
Note that enum types may not be coerced or cast to variant types. Instead, matching must be
performed to guarantee that the enum type truly is of the expected variant type.
enum Sum { A(u32), B, C }
let s: Sum = Sum::A;
let a = s as Sum::A; // error
let a: Sum::A = s; // error
if let a @ Sum::A(_) = s {
// ok, `a` has type `Sum::A`
println!("a is {}", a.0);
}
If multiple variants are bound with a single binding variable x
, then the type of x
will simply
be the type of the enum, as before (i.e. binding on variants must be unambiguous).
Variant types interact as expected with the proposed
generalised type ascription (i.e. the same as type
coercion in let
or similar).
Type parameters
Consider the following enum:
enum Either<A, B> {
L(A),
R(B),
}
Here, we are defining three types: Either
, Either::L
and Either::R
. However, we have to be
careful here with regards to the type parameters. Specifically, the variants may not make use of
every generic parameter in the enum. Since variant types are generally considered simply as enum
types, this means that the variants need all the type information of their enums, including all
their generic parameters. This explictness has the advantage of preserving variance for variant
types relative to their enum types, as well as permitting zero-cost coercions from variant types to
enum types.
So, in this case, we have the types: Either<A, B>
, Either<A, B>::L
and Either::<A, B>::R
.
Links and related work
- enum variant support rust#89745 (draft PR)