[Pre-RFC] Anonymous enum

Quite often I find the need to return values of different types from a function, where all the possible return types implement some common traits. This comes up frequently with futures, where different code paths need to return a different type of future.

The current approach is to return Box<Trait> or define an enum manually and write a forwarding implementation of the correct traits, both of which are suboptimal for different reasons.


My suggestion is to add an anonymous enum type (A | B) which automatically implements every trait which is implemented by both A and B, but user code is not allowed to match manually on the enum.

// the enum keyword packs the value into an anonymous enum
let x : (&str | u64) = if cond { enum "Foo" } else { enum 5 };
println!("{}", x); // x implements Display, since both &str and u64 do

This fits really well with impl Trait, even if they are orthogonal features. In this example, the handle function’s real return type is (future::Map<db::DBQuery, {closure}> | future::FutureResult<String, Error>)

fn handle(path: &str) -> impl Future<Item=String, Error=Error> {
    match path {
      // DB::query returns some future with type DBQuery
      "/" => enum self.db.query().map(|data| data.to_string()),
       _  => enum future::finished("404 Not found".to_owned()),
    }
}

We can even go a step further, using an anonymous enum as the future’s item type. The real return type is now ( future::Map<(&'static str | DBData), {closure}> | future::FutureResult<(&'static str | DBData), Error>)

fn handle(path: &str) -> impl Future<Item=impl Display, Error=Error> {
    match path {
      // DB::query returns some future with type DBQuery
      "/" => enum self.db.query().map(|data| enum data),
       _  => enum future::finished(enum "404 Not found"),
    }
}

Yay, no boilerplate and no allocations required !


Unfortunately, it’s not all as nice as it seems. I ran into issues trying to determine which traits can be automatically implemented, and how.

I initially thought object-safe traits would be fine. Just match on *self and call the method on the underlying type. However object-safe traits can still have where Self: Sized non object-safe methods which cannot (systematically) be implemented. Since the enum would be Sized, it needs to define these methods. Restricting automatically implemented traits to those which only contain “object-safe methods” would exclude Future and Iterator, defeating the whole point.

For these two traits, all the where Self: Sized methods also have a default implementation. Therefore instead of dispatching to the underlying types the default implementation can be used instead. That is how the impl <T: ?Sized + Future> Future for Box<T> works for instance. However while it is fine with Future and Iterator I don’t think prioritising the default implementation over the underlying types’ implementations is desirable in all cases, so it shouldn’t be used for automatic generation of the trait implementations.

There are also non object-safe but very useful traits which could be implemented, albeit maybe not automatically by the compiler. Clone and Into are some examples.


Overall I think

  • Traits which only have “object-safe methods” (this is a stronger requirement than object-safe trait) can be implemented automatically, Deref or Display for example
  • Trait authors should have a way to provide an implementation manually (Future, Iterator, Clone, Into)
  • Some traits simply cannot be implemented at all (eg static methods, methods which take two Self values)

Note that the manual implementation only needs to be done once per trait, as opposed to once per enum like it is with manually defined enums.

Regarding manual implementation, I’ve been thinking about something like that :

// Implements Clone for anonymouse enums of any arity
// where all underlying types are Clone
impl <T: Clone> Clone for (T|...) {
  // self has type &(T|...)
  // gets replaced by the real type of the enum during reification
  fn clone(&self) { 
    // Ugly imaginary syntax ahead, please bikeshed
    // v is the underlying value, X the underlying type
    enum_match *self => ref v : X {
      // v.clone() returns an X
      // The enum keyword packs it into a anonymous enum
      enum v.clone()
    }
  }
}

impl <T: Future> Future for (T|...) {
  type Item = T::Item;
  type Error = T::Error;
  fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
    enum_match *self => ref v : X {
      v.poll()
    }
  }

  /* 
   * Rely on default impl for all the other methods of the Future trait,
   * rather than forward to the underlying value. If it wasn't for these
   * methods, the compiler could have generated this impl block
   */
}

enum_match can only be used in a impl Trait for (T|...)

Given anonymous union (A | B), this gets reified into the following

impl <A, B> Clone for (A | B)
  where A: Clone, B: Clone
{
  fn clone(&self) {
    // enum_match gets expanded into a match with one an arm
    // per underlying type
    match *self {
      (A | B)::A(ref v) => (A | B)::A(v.clone()),
      (A | B)::B(ref v) => (A | B)::B(v.clone()),
    }
}

impl <A, B> Future for (A | B)
  where
    A: Future,
    // Associated types must match
    B: Future<Item=A::Item, Error=B::Error>,
{
  type Item = A::Item;
  type Error = A::Error;
  fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
    match *self {
      (A | B)::A(ref v) => v.poll(),
      (A | B)::B(ref v) => v.poll(),
    }
}

Note that this cannot be done in user code, since enums can’t normally be pattern matched on.

Anyway, just some idea I had. I interested to see what people think

2 Likes

There have been three attempts at something like this over the years that have a lot of interesting discussion attached:

https://github.com/rust-lang/rfcs/pull/402 https://github.com/rust-lang/rfcs/pull/514 https://github.com/rust-lang/rfcs/pull/1154

1 Like

The biggest difference between my proposal and the previous ones is that the enum can’t be pattern matched, only accessed through traits. That design decision is motivated by impl Trait and futures-rs’ design, which weren’t around when these proposals were written.

Tbh I was quite disappointed myself by my proposal when I realised automatic trait dispatch implementation wouldn’t be that simple. I thought I’d still share my thoughts to get some feedback

Is enum really needed here? Maybe something simpler would feel more natural:

let x: some Display = if cond { "Foo" } else { 5 };

The anonymous enum becomes an implementation detail, but is there anything that they can express that an existential couldn’t?

5 Likes

Stepping back from anonymous enums to this particular use case: the Futures library contains an Either future, which is an enum with the behavior you want. I have a PR against futures to add a left and right combinator to Future to make it a bit more ergonomic to use.

However, when I discussed this PR with @aturon he mentioned that, as @Letheed has just suggested, we could implicitly unify multiple types with an anonymous enum when we are trying to evaluate them as an existential. We might not have explicit syntax for creating an anonymous enum, but it would exist as an implementation detail of impl Trait.

1 Like

@Letheed I like the idea but I think the syntax you’re using is a bit too implicit. For one, type ascriptions aren’t really supposed to coerce. I wonder if some form of unify keyword would be appropriate:

let a: some Display = if cond { unify "Foo" } else { unify 5 };

What about trait objects?

let a: Box<Display> = if cond { box "Foo" } else { box 5 };

Yes, the implicitness is the only downside i’ve found. OTAH it feels natural to use and should be easy to learn/teach. I’d rather we didn’t introduce another keyword if we don’t have to. I could live with it though. It’s just that unless we make the anon enums explicit in Rust (with maybe the pipe for ex.) it seems redundant with the existential quantifier.

1 Like

I have to agree a silent coercion is going too far, but I actually really like this idea, so maybe add just one extra keyword like enum to make it “explicit”?

let a: enum some Display = if cond { "Foo" } else { 5 };

fn foo() -> enum Some Display {
    if cond { "Foo" } else { 5 }
}

I don’t agree that “silent coercion” is a problem any more than its a problem that Box<Trait> becomes a fat pointer with a vtable.

Coercions are a problem when they let you ‘loose track’ of what type your value is (or worse - though we would never - when they can actually lose information, like a Long coercing to a Double). This is a change in type only insofar as it changes the representation of the type, but its still an impl Trait, you can’t do anything with it you couldn’t without the coercion.

1 Like

Agreed. This is exactly what we already do with boxed traits.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.