[Idea] Moving the `Either` type into `core`?

Forwarded from users.rust-lang.org:

Coming from this post I'm wondering, given the popularity of https://crates.io/crates/either:

  • Is it possible to move Either into core for better visibility?
  • What could be preventing this from happening?

Thanks in advance :slight_smile:

(Thanks @kornel for mentioning that std::either::Either existed back in 2014, but was removed in Remove std::either · rust-lang/rust@4bea679 · GitHub)

3 Likes

The reasoning for removal given at the time:

It's rarely used and usually it's better to define a custom enum type.

I agree. I can't think of a situation where using Either is preferable to defining a more informative type.

But there was also some discussion of how it may be useful:

A public API might be easier to use if Either is used as the recurring alternative type, just like tuples are sometimes used instead of creating custom types for each time a pair of values returned.

3 Likes

The primary value of the crate looks to me like it's the monad-like methods (like for Option and Result)

The general solution, it would then seem, would be to automatically generate them somehow for user enums. Sounds like a proc macro someone wrote already?

The more language-y option is probably something to do with letting generic methods take enum alternatives somehow?

The most common use I’ve seen of Either is in functions that return impl Trait: If the function body produces one of two types that both implement the named trait, then Either can be used as the anonymous return type (assuming that Either implements the named trait).

9 Likes

The only times I've seen that have been for crate_name::Either implements crate_name:: SomeTrait - making Either a core type doesn't by itself seem to provide much value as you only save the 4 lines of the enum definition itself, and perhaps the user having to import it?

It does seem that the "ideal" core Either should be able to blanket impl "every" trait, though, which would be really nice. No idea what that looks like in actual Rust!

1 Like

I think that the most common single trait I’ve seen Either used for is Iterator, where a function wants to return one of two different sequences based on a condition, and those two sequences are built through complicated adapter chains. The other implementations for std traits are similarly useful, but any individual trait implementation is fairly easy to replicate for a custom enum.

2 Likes

Huh. I always did that with Chain<Option, Option> but std::iter::Either would in isolation make more sense.

I did some digging, looks like this earlier thread covers the topic pretty well: [lib] Can we add an enum `Either` in std?

1 Like

If Either could have been a special type that magically delegates every trait implemented by its variants, that would be really cool.

But such magic could go further and have dedicated syntax:

or delegation could make DIY Either-like types easy to make:

16 Likes

A big difference with this is that Either is binary while tuples are variadic. A variadic enum Any<...T> would a closer analog for these sorts of usecases (rather than having to nest Either<A, Either<B, Either<C, D>>>).

1 Like

The usual syntax when discussion for "structural sum types" ("ad-hoc enums") is (TLeft | TRight).

There's however multiple big open questions with the proposal/idea that people disagree on, e.g.

  • Do they inherit trait implementations? How so? How does this impact trait stability?
  • How do you pattern match on a structural enum?
  • What, if any, collapsing of variants is done?
    • Are (A | B) and (A | B | B) different types?
    • Are (A | (B | C)) and (A | B | C) different types?
    • How does this impact how they are used?
    • How does this interact with generics?
  • How does this interact with the existing nominal enums?
    • Or the "enum variants are types" proposal?[1]
    • Or the "types as enum variants" proposal?[2]

Just adding Either to core sidesteps basically all of the design complexities of structural sum types by virtue of being nominal and a fixed airity.


However, I think the desire here is better served by "enum impl Trait" allowing you to return multiple types implementing to the same trait as a fresh existential type (along with TAIT to be able to give the type a name).

Alternatively, this'd also be served well by some -> dyn Trait; this would compile as some -> StackBox<dyn Trait, [uAlign; SIZE]> with compiler inferred static size/align. This is very similar to -> enum impl Trait[3] but IMHO a tiny smidgen better[4], as the dyn offers a clear indication of only working for object-safe traits and the dynamic dispatch involved. (In theory the optimizer could even devirtualize back to enums... but we have a track record of enum dispatch being significantly faster than dyn dispatch.)

And on that parenthetical: this usecase should also consider if it's served by the enum_dispatch crate. The technique it uses to delegate the trait implementation is brittle[5], but it's a good example of what can be done in userspace.


  1. Enum::Variant is a subtype of Enum which is only the named variant, and Enum::Variant coerces to Enum; IIRC this is (experimentally?) accepted but not yet implemented. ↩︎

  2. The inverse of the previous, using existing types as enum variants; basically sugar for newtype variants; never actually RFCd; just discussed as potential design space. ↩︎

  3. And similarly, might could be spelled enum dyn Trait to separate it from actual unsized returns/locals ↩︎

  4. Although to be fair, I am somewhat biased as an author of such "inline unsized type" library support and champion of the "storage API" which makes Box itself usable as StackBox ↩︎

  5. It will break if proc macro invocations get sandboxed from each other, as it uses global state to remember between invocations on the enum and the trait; it'd be much more resilient to use the technique used by ambassador for delegating trait implementations with a decl macro instrad. ↩︎

8 Likes

Another interesting crate in this area is auto_enums.

6 Likes

One possibility to make it easier to implement traits for Either is to allow #[derive(Either)] on traits. I know, currently traits are derived for types, not the other way around, but there's nothing preventing us from allowing that as well.

If the Either enum and the derive macro is in the standard library, that would make it very easy to implement custom traits for Either.

If we don't want to allow #[derive] on traits, a normal attribute macro, like #[derive_either], would work, too.

3 Likes

You match them with : like this of course:

fn is_truthy(input: &str |  i32 | bool) -> bool {
     match input {
           number: i32 => number != 0,
           boolean: bool => boolean,
           string: &str => !string.is_empty(),
     }
}

Though I agree with you that just having "enum impl Trait" would probably be way better and much simpler to implement. I've also used Either for Iterators, mostly because I didn't know the auto_enums crate existed.

4 Likes

The biggest issue with a single Either type—no matter where it lives—is that you can't have both

impl<A, B> IntoIterator for Either<A, B> where A: IntoIterator, B: IntoIterator { … }

and

impl<A, B> Iterator for Either<A, B> where A: Iterator, B: Iterator { … }

even though both of those would be useful :slightly_frowning_face:

4 Likes

Yeah, and that is why Either only has an inherent into_iter() method, since either#12.

1 Like