- Feature Name:
anonymous_enums
- Start Date: 2020-02-04
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Add A | B
syntax that represents anonymous enum like enum Either<A, B> { Left(A), Right(B) }
. This feature has a lot in common with tuple syntax (A, B)
which is anonymous strust like struct Tuple<A, B>(A, B)
Note: this feature was previously rejected at least 3 times:
However this RFC differs a lot from previous.
Rust supports both kinds of composite algebraic data type: product types (structs) and sum types (enums). Rust has anonymous structs (tuples) but no anonymous enums, leaving a gap in it's type system.
named | anonymous | |
---|---|---|
products | structs | tuples |
sums | enums | ??? |
exponentials | functions | closures |
This RFC resolves this gap.
In rust, when you need to accept or return either of some types, it's common to use enums.
However in some cases this aproach leads to a lot of boilerplate and crates like either
and frunk
.
This feature could reduce this builerplate.
Also, when you have fn f() -> impl Trait { ... }
that, depending on some condition, return either of some types, you also need to return enum.
Which, again, leads to a lot of boilerplate (see e.g. futures::future::Either
)
// TODO: better write motivation
struct A;
struct B;
let it: A | B = ::0(A);
match it {
::0(a) => ...,
::1(b) => ...,
}
// desugars to
let it: AnonymousEnum2<A, B> = AnonymousEnum2::_0(A);
// ^^^^^^^^^^^^^^ ---- owned by core
match it {
AnonymousEnum2::_0(a) => ...,
AnonymousEnum2::_1(b) => ...,
}
The ::n
syntax was chosen because it
- mirrors tuple's
.n
syntax - shows that this is a type/enum variant (like
Enum::Var
but withEnum
ommited since it's anonymous)
fn read<T>() -> Result<T, io::Error | T::Err>
where
T: FromStr,
{
// There are 2 thing you need to note:
// 1. We can't use `?` dicerectly, since `A | B` can't implement both `From<A>` and `From<B>` :(
// 2. We can use `::0` as a function, just like `Either::Left`, `Enum::Var`
let string = read_file().map_err(::0)?;
T::from_str(&string[..]).map_err(::1)
}
Because ?
works badly with anonimous enums this example isn't as argonomic as it could be,
but it's still a good alternative for using Either
or creating new error types. (both of which are vulnerable to the same)
This works for any number of variants:
let it: T0 | T1 | ... | Tn = ::x(Tx);
match it {
::0(_) => ...,
::1(_) => ...,
...
::n(_) => ...,
}
This RFC doesn't propose any kinds of flattening i.e. if T
is B | C
, then A | T
is not A | B | C
:
type T = B | C | D;
let it: A | T | E = ::1(::0(B));
match it {
::0(a) => ...,
::1(t) => match t {
::0(b) => ...,
::1(c) => ...,
::2(d) => ...,
}
::2(e) => ...,
}
// or
match it {
::0(a) => ...,
::1(::0(b)) => ...,
::1(::1(c)) => ...,
::1(::2(d)) => ...,
::2(e) => ...,
}
This code is a bit tricky, but anonymous enums aren't expected to be used with a lot of variants/big nestting, so I hope this is ok.
Some traits like Copy
, Clone
, Debug
, Eq
, Hash
, etc obviously should be implemented for T0 | ... | Tx
where T0: Trait, ..., Tx: Trait
just like for tuples and arrays.
And just like for tuples and arrays there probably should be some limit on number of types.
Trait that also could be implemented, but it's not that obvious:
Iterator
Future
- TODO: continue the list
Note that anonimous enums can only implement object safe traits, so it's impossible (at least resonably) implement traits like Default
or From
.
Also semantics of Ord
are questionable.
Also, see Implementing traits for A | ... | T
.
There is Demo of anonymous enums simulated with macros.
fn fun(arg: Ae![i32 | i32 | String]) -> String {
match arg {
ae_pat!(::0(int)) | ae_pat!(::1(int)) => int.to_string(),
ae_pat!(::2(string)) => string,
}
}
#[test]
fn it_works() {
assert_eq!(
fun(ae!(::0(1))),
"1"
);
assert_eq!(
fun(ae!(::1(42))),
"42"
);
assert_eq!(
fun(ae!(::2(String::from("hi")))),
"hi"
);
}
(and more)
// TODO
// TODO
- This RFC adds quite a complicated feature.
- Syntax is quite unintuitive (but I couldn't find better syntax that works well with
A | A
and generics)
// TODO
As said there were 3 takes to this feature before this. This RFC tryes to resolve problems from previous tryes, but also adds some questions.
Comprison table:
problems/properties | First | Second | Third | This |
---|---|---|---|---|
A | A is illegal and so doesn't work with generics |
⭕️ | ⭕️ | ✅ | ✅ |
is not the same as enum Either<A, B> { Left(A), Right(B) } * |
✅ | ⭕️ | ✅ | ✅ |
works badly with ? |
✅ | ✅ | ⭕️ | ⭕️ |
pattern syntax for A | B |
a @ A |
a as A ;both as A + B |
(a|!) |
::0(a) |
Allows calls to trait methods implemented by both types** |
allow | allow | disallow | disallow |
Allow coersion A to A | B *** |
allow | allow | disallow | disallow |
Creation syntax for A | B |
A ;A as (A | B) |
A ;A as (A | B) |
(A|!) |
::0(A) |
Allow single variant enum | disallow | disallow | allow | unresolver |
Allow zero variant enum | disallow | disallow | disallow | unresolver |
(⭕️ - bad, ✅ - nice)
*
in second RFCA | B
means eitherA
,B
or(A, B)
. Thats unintutive.**
infirst
andsecond
RFCs, ifA: Trait
andB: Trait
, then you can call object-safe methods onA | B
. However this brings a lot of questions like "is(A | B): Trait
?", "exactly which traits/methods should be allowed?", so was removed later.***
infirst
andsecond
RFCs its allowed to writelet _: A | B = A;
, but there 2 problems:- this means that
let x = A;
doesn't guarantee thatx
has the typeA
, because it can be later required to beA | ...
- it doesn't work when
A | A
is allowed
- this means that
// TODO
- Should one variant enum be allowed?
(A |) ~= enum _<A> { _0(A) }
- Should empty enum be allowed?
(|) ~= ! ~= core::convert::Infallible
With this feature it's also reasonable to add anonymous variants syntax to common enums i.e.:
enum Enum(A | B); // or `(A, B)`?
let it: Enum = Enum::0(A);
match it {
Enum::0(_) => ...,
Enum::1(_) => ...,
}
It's possible to write a macro (maybe not in std) that automaticaly implements "simple" traits by signature, e.g.:
impl_enums! {
pub trait Debug {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
}
// expands to
impl<A, B> Debug for A | B
where
A: Debug,
B: Debug,
{
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
::0(inner) => inner.fmt(f),
::1(inner) => inner.fmt(f),
}
}
}
...
impl<T0, T1, ..., Tx> Debug for T0 | T1 | ... | Tx
where
T0: Debug,
T1: Debug,
...
Tx: Debug,
{
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
::0(inner) => inner.fmt(f),
::1(inner) => inner.fmt(f),
...
::x(inner) => inner.fmt(f),
}
}
}
Where x
is some limit, like 32
for arrays or 12
for tuples.
By "simple" I mean "object safe" and without generics/associated types.