Accessing enum variant trait method without matching

I have an enum, all variants of which implement the same trait. Is there a way to access the trait methods without matching all the variants?

trait Trait {
    fn method(&self) -> bool {
        true
    }
}

struct Foo;
impl Trait for Foo {}
struct Bar;
impl Trait for Bar {}

enum Enum {
    Foo(Foo),
    Bar(Bar),
}

fn main() {
    let variant = Enum::Foo(Foo {});
    // is it possible to do the same without matching all the possible variants
    // knowing that all the variants implement the trait?
    match variant {
        Enum::Foo(x) => x.method(),
        Enum::Bar(x) => x.method(),
    };
}
1 Like

You need some sort of match here (whether it's behind a helper method, if-let, or whatever) because Foo and Bar are fundamentally different types.

Because they both implement the same trait and that trait is object-safe you could do something like this:

trait Trait { fn do_stuff(&self); }

struct Foo { ... }
struct Bar { ... }

enum FooOrBar {
  Foo(Foo),
  Bar(Bar),
}

impl FooOrBar {
  pub fn do_stuff(&self) {
    self.as_trait().do_stuff();
  }

  fn as_trait(&self) -> &dyn Trait {
    match self {
      FooOrBar::Foo(f) => f,
      FooOrBar::Bar(b) => b,
    }
  }
}
2 Likes

Yeah, that would do, but unfortunately my real trait can't be made into an object

Could you implement your trait for your enum? That way all your pattern matching is in one place

5 Likes

It could be a solution, but in my particular case the trait is used to provide some defaults for inner structs:

trait Step {
    fn num_attempts(&self) -> u16 {
        1
    }
}

struct StepFoo;
impl Step for StepFoo {}

enum Steps {
    Foo(StepFoo),
}

I've chosen this structure as all the information about a step lives in one place. But if I implement the trait for the enum - a part of logic would move there. Proxying a few trait method in the enum implementation, while verbose, still feels better, as it mindless. If I add a new step and forget to write the proxy method, compiler will remind me, as there's an exhaustive match there. Also the verbosity can probably be mitigated by a macro.

Why would it? You don't have to inline nor duplicate all of the interesting logic, and you shouldn't: just forward it directly to the associated values of each variant!

impl Trait for MyEnum {
    fn complicated_computation(&self) -> u32 {
        match *self {
            MyEnum::Foo(ref x) => x.complicated_computation(),
            MyEnum::Bar(ref y) => y.complicated_computation(),
        }
    }
}
4 Likes

Yep, that what I mean by proxying, but instead of implementing the trait, I just added those methods directly into the enum implementation. Maybe I got @RustyYato 's advice wrong, but I thought they suggested to move trait implementation from inner structures to the enum. So inner structures won't have the trait implemented and I'd use enum variants instead of them.

The typical approach is as @H2CO3 indicated, and what I believe @RustyYato actually meant, you just implement the trait on your enum and dispatch (proxy) to the variants within, a sort of boiler-plate form of dynamic dispatch. Both the enum and the types wrapped in variants have the trait implemented.

3 Likes

Yeah, I see now, but my initial question was exactly about possibility to avoid matching all the variants inside of each proxy method :slight_smile:

Matching in this case isn't optional. The reason is that before matching, you don't know which variant you're dealing with, and matching allows for solving that problem.

2 Likes

The knowledge about the variant wouldn't be necessary if compiler knew that all the variants implement this trait. But I guess there's no way to let it know for now.

There is: implementing the trait. But as @H2CO3 already indicated in one of the replies, that needs to use match as well.

As a side note: why are you so focused on not using a match? It's a fundamental building block for rust code.

why are you so focused on not using a match?

Because they add logically unnecessary num_variants * num_trait_methods lines of code (places for bugs).

The only place that I can see for bugs in a match expression is in the guards, if there are any. And there wouldn't be any here.

The fact that the enum is exhaustive ensures that the match must be exhaustive as well. This means that if you change any of the variants, or add or remove a variant, you'll get compile errors at the places where you use match on the variants of that enum.
This is in stark contrast to e.g. switch-case in almost all other mainstream languages: changing the enum in those PLs wouldn't result in a compile error at all.

All in all, not really a lot of space for bugs to creep in when dealing with exhaustive enums.

2 Likes

Here's an example of an error:

trait Trait {
    fn min(&self) -> usize {
        1
    }
    fn max(&self) -> usize {
        100
    }
}

struct Foo;
struct Bar;

impl Trait for Foo {}
impl Trait for Bar {}

enum Enum {
    Foo(Foo),
    Bar(Bar),
}

impl Trait for Enum {
    fn min(&self) -> usize {
        match self {
            Enum::Foo(x) => x.min(),
            Enum::Bar(x) => x.max(),
        }
    }
}

If you are worried about those kinds of errors, write a #[derive] macro that automatically generates the implementation of the trait for the enum.

9 Likes

You might be interested in the enum_dispatch crate, it might be able to help eliminate the boilerplate you're worried about.

With your example it would look something like:

use enum_dispatch::enum_dispatch;

#[enum_dispatch(Enum)]
trait Trait {
    fn method(&self) -> bool {
        true
    }
}

struct Foo;
impl Trait for Foo {}
struct Bar;
impl Trait for Bar {}

#[enum_dispatch]
enum Enum {
    Foo,
    Bar,
}

fn main() {
    let variant = Enum::Foo(Foo {});
    variant.method();
}
3 Likes

A common way to avoid this duplication is to make a macro_rules! macro to help.

For example, take a look at how the either crate is implemented:

3 Likes

Unfortunately enum_dispatch doesn't accept traits with static members (which my trait has)

I ended up splitting static members into utility function and using enum_dispatch. It also provides From<InnerStruct> implementations for all the variants.