Skip to content

Latest commit

 

History

History
353 lines (272 loc) · 12.8 KB

0000-anonymous-enums.md

File metadata and controls

353 lines (272 loc) · 12.8 KB

Summary

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.

Motivation

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

Guide-level explanation

Basic concept

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 with Enum ommited since it's anonymous)

Working with errors

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)

More than 2 variants

This works for any number of variants:

let it: T0 | T1 | ... | Tn = ::x(Tx);
match it {
    ::0(_) => ...,
    ::1(_) => ...,
    ...
    ::n(_) => ...,
}

Flattening

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.

Automatic Trait Implementation

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.

Demo

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)

Reference-level explanation

// TODO

Repr

// TODO

Drawbacks

  • 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)

Rationale and alternatives

// 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 RFC A | B means either A, B or (A, B). Thats unintutive.
  • ** in first and second RFCs, if A: Trait and B: Trait, then you can call object-safe methods on A | B. However this brings a lot of questions like "is (A | B): Trait?", "exactly which traits/methods should be allowed?", so was removed later.
  • *** in first and second RFCs its allowed to write let _: A | B = A;, but there 2 problems:
    1. this means that let x = A; doesn't guarantee that x has the type A, because it can be later required to be A | ...
    2. it doesn't work when A | A is allowed

Prior art

// TODO

Unresolved questions

  • Should one variant enum be allowed? (A |) ~= enum _<A> { _0(A) }
  • Should empty enum be allowed? (|) ~= ! ~= core::convert::Infallible

Future possibilities

Anonymous variants in common enum

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(_) => ...,
}

Implementing traits for A | ... | T

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.