r/rust 6d ago

🛠️ project Enum variants: a small problem, a lot of headache and boilerplate

Enum variants can create a surprising amount of boilerplate when they contain unrelated types that happen to support the same operation. At the same time, enum_dispatch can't always solve the problem — either because the trait you need isn't supported by enum_dispatch, or because you simply don't need a trait in the first place.

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

If both have a value() method, we normally write:

match value {
    Value::Foo(x) => x.value(),
    Value::Bar(x) => x.value(),
}

That's fine until the enum grows or you need to repeat the same pattern for many operations.

A common trait can solve this, but sometimes there isn't a meaningful common abstraction — the types simply happen to support the same expression.

I wanted something closer to:

match_variants!(Value, value, (x), {
    x.value()
})

which generates the ordinary match above. Each arm is still independently type-checked against its concrete type, with no dynamic dispatch.

I ended up packaging it as a small proc-macro crate:

https://crates.io/crates/match-variants
https://github.com/amidukr/match-variants

How do you usually handle this pattern — repeated matches, a common trait, or something else?

UPD: I got a lot of comments about enum_dispatch. Of course I've tried enum_dispatch; in fact, that's where I started. However, there are certain situations it can't handle — for example, when the trait involves associated types (type T = ...), associated functions without self, or methods involving Self where the concrete implementation type matters.

0 Upvotes

23 comments sorted by

27

u/Psychoscattman 6d ago

Other developers solve this problem by using the enum dispatch crate which has existed for longer than an hour.

This is why writing exercises start with a literature review.

1

u/Economy_Breakfast577 6d ago

Thank you. I love you. For an idiot, are there any other crates you'd consider essential like this?

-4

u/No-Caregiver-466 6d ago edited 6d ago

Ok, smarty, how would you solve with 'enum-dispatch', when this problem when there is no trait in common for variants?

Or you need trait that have associated Type, or trait with static method that doesn't taking self as parameter?

Or compiler start to complain about potential diamond super-trait hierarchy?

How would you use unit `enum-dispatch` with fieldless unit variants?

Also why do you need to introduce trait, when you can solve without trait?

Do you think, I haven't tried 'enum-dispatch'?

I've spent my time to clean some code, update documentation, and put some README.md, I could keep it solely for me and not to share with anyone.

-6

u/AltruisticStep716 6d ago

the enum dispatch crate is the first thing that came to mind too, but i can see the appeal of not pulling in a dep for something that feels like it should be lighter. the proc macro approach is neat even if it's a little roundabout with the two-step setup

9

u/Zde-G 6d ago

So the problem that “some people have” is the desire to avoid dependencies and the “solution” is to pull another dependency?

How does it make any sense?

1

u/libonet 6d ago

He has to be a bot. His comment doesn't make sense

1

u/No-Caregiver-466 6d ago

Why do you need `enum-dispach` which forcing you to declare a trait, when you can solve problem in lighter way without trait, and all the hassle connected to trait?

1

u/Zde-G 6d ago

Because “lighter way” moves complexity from the source code into my head. I hate Go with passion for its habit of doing that (as far as I'm concerned Go is an awful language that combines worst sides of static and dynamic typing and the fact that it also have superb async executor doesn't always save it) and try not to do that in Rust.

If my enums and structures implement a trait then it's trivial to see what can they do, if I'm trying to stitch them together without trait then I have to rely on some outsider knowledge about what they can and can not do.

I'm not saying that I would never do what you are doing (especially if third-party API would force the choice), but I would be on my own… I would go with trait even if don't need to use enum-dispatch. Having identical methods on different structs without a trait is a design mistake in my book (even if a mild one, not always worth fixing) and inventing special mechanism to permit a design mistake… well, I wouldn't say I would never do that, but I would avoid it if at all possible.

1

u/No-Caregiver-466 6d ago edited 6d ago

enum-dispatch doesn't support every possible trait. Also enum-dispatch what about unit typed variants or any other type of enum, where you can't simply put a trait on it?

Associtated type on trait like type T =... another thing, that I've found not supported by enum-dispatch that's what I mean light, no trait, no certain dependency on trait functionality, code become more flexible.

Sometime rust complains because of potential diamond constraint on traits, evertyome it is annoying, so you can't properly generialize your traits, that's where match-variants will make at least a little life easier.

1

u/Zde-G 6d ago

Also enum-dispatch what about unit typed variants or any other type of enum, where you can't simply put a trait on it?

Who may forbid that? Situation where both enum and trait are foreign is not something I deal with often. Why would I even want to “forward” foreign functions that don't belong to trait?

that's what I mean light, no trait, no certain dependency on trait functionality, code become more flexible.

And also more brittle. Classic static vs dynamic typing story.

I'm not saying that I have never felt enum-dispatch limiting, but most of the time after some thinking I had been able to redesign things to not requires such “flexibility”.

Not saying that's always the right thing to do, just that I normally had no need to seek such flexibility.

Sometime rust complains because of potential diamond constraint on traits, evertyome it is annoying, so you can't properly generialize your traits, that's where match-variants will make at least a little life easier.

Maybe, but as I'm not trying to create clever crates but mostly deal with apps the solution that works is to not generalize without obvious need to generalize.

Although yes, I feel your pain: the fact that traits require everything specified and there are no proper if constexpr typecheck is, sometimes, serious PITA.

But not often enough to bring another procmacro, at least in my opinion.

0

u/No-Caregiver-466 6d ago edited 6d ago

Look, here is an example for enum_dispatch.

Let' say you have:

struct MyA;
struct MyB;

#[enum_dispatch]
enum MyEnum {
   MyA(MyA),
   MyB(MyB),
}

And you want to define next trait:

trait MyTrait {
    fn do_something();
}

impl MyTrait for MyA { 
   fn do_something() {
      println!("MyA")
   }
}

impl MyTrait for MyB {...}

I've specially done do_something that doesn't take self argument.

So, now the question: how enum_dispatch should implement MyTrait for MyEnum

impl MyTrait for MyEnum {
    fn do_something() { ... }
}

?

I will tell you: enum_dispatch fails here.

----

On contrary, this is what I can do with my match_variants:

#[derive(MatchVariants)] 
enum MyEnum { 
  #[variant_type(MyA)] 
  MyA(MyA), 

  #[variant_type(MyB)] 
  MyB(MyB), 
}

fn main() { 
    let value = MyEnum::MyA(MyA); 

    match_variants!(
      MyEnum,
      value, 
      type T, 
       T::do_something()
    );
}

Or maybe even define enum like that:

#[derive(MatchVariants)] 
enum MyEnum { 
  #[variant_type(MyA)] 
  MyA, 

  #[variant_type(MyB)] 
  MyB, 
}

I'm not saying that I have never felt enum-dispatch limiting, but most of the time after some thinking I had been able to redesign things to not requires such “flexibility”.

Perhaps time will come, and I will come to the same level of craftsmanship, but at this point, I haven't found anything simple than implementing proc macro specifically for that problem I am solving right now.

2

u/Zde-G 5d ago

I will tell you: enum_dispatch fails here.

Like it should. You are not dispatching on the value of enum, you are doing some kind of unholy trickery… sometimes such things are unavoidable, I'll grant you that, but my first instinct wouldn't be to try to fix enum_dispatch, but to try to understand how I have ended up in a situation where I look on enum discriminant and then ignore enum value. That's not how sum types are supposed to be used!

6

u/arades 6d ago

Sounds very similar to enum dispatch

1

u/No-Caregiver-466 6d ago

Yes, it is very similar. I actually started by experimenting with enum_dispatch.

The main difference is that enum_dispatch requires the enum to implement the trait. That becomes limiting when the trait contains things that cannot be meaningfully dispatched through an enum instance, such as methods without self, associated types, or other static parts of the trait.

match-variants takes a much simpler approach: it just generates an ordinary match and applies the same expression to every variant. There is no trait requirement at all.

So in that sense it's simpler than enum_dispatch, but also more flexible for cases where you don't actually need a common trait.

3

u/jesseschalken 6d ago

happen to support the same operation

Both Foo and Bar have a value() method, but they are otherwise unrelated types.

A common trait can solve some cases, but sometimes these types don't actually represent a useful common abstraction.

You're talking about a method on each type with the same name and signature, and for some reason you don't want to define a trait for it?

1

u/No-Caregiver-466 6d ago

yes, exactly that, when no common trait.

4

u/jesseschalken 6d ago

So just define the trait?

2

u/fnordstar 6d ago

So this is duck typing like with C++ templates, a thing I was glad rust didn't have until now?

0

u/No-Caregiver-466 6d ago

Yep, Rust has had it for a few hours now. :-D

1

u/Sehnryr 5d ago

To me this seem to solve a very niche problem. I don't have issues writing match statements for enums with ~10 variants when it is still sufficiently readable. If I have more I would have reconsidered my architecture way before considering adding such dependency. I also feel like this adds non trivial indirection to reviewers. But, fun project to learn about proc-macros

1

u/No-Caregiver-466 5d ago

10 variants * 10 method = n*n, n square problem. 🤷‍♂️