Enum variants can create a surprisingly annoying problem when they contain unrelated types that happen to support the same operation.
Consider:
enum Value {
Foo(Foo),
Bar(Bar),
}
Both Foo and Bar have a value() method, but they are otherwise unrelated types.
The straightforward Rust solution is:
match value {
Value::Foo(x) => x.value(),
Value::Bar(x) => x.value(),
}
Nothing is wrong with this code. The problem starts when the enum grows, or when you need to perform many different operations this way.
You end up repeatedly writing essentially the same match:
match value {
Value::Foo(x) => x.some_operation(),
Value::Bar(x) => x.some_operation(),
}
A common trait can solve some cases, but sometimes these types don't actually represent a useful common abstraction. Introducing a trait only to avoid repeating match arms feels like solving a syntactic problem by changing the type design.
dyn Trait also isn't what I wanted here. The enum already provides static dispatch, and I wanted to preserve concrete types in every branch.
I started wondering whether Rust could express something closer to:
match_variants!(Value, value, (x), {
x.some_operation()
})
and have it expand into:
match value {
Value::Foo(x) => x.some_operation(),
Value::Bar(x) => x.some_operation(),
}
The interesting part is that x has a different concrete type in every generated match arm, so the expression can be independently type-checked for each variant. No common trait or dynamic dispatch is necessary.
This turned into an interesting proc-macro problem of its own.
A function-like proc macro can see the expression passed to it, but it can't ask rustc which variants belong to an arbitrary enum type. A derive macro can see all the variants, but it doesn't know what expression will later be applied to them.
The solution I ended up experimenting with is combining the two: #[derive(MatchVariants)] records the variant information in a generated helper macro, and match_variants! later uses that information to generate the actual match.
I packaged the experiment as match-variants:
https://crates.io/crates/match-variants
Source:
I'm curious how other Rust developers approach this pattern. Do you usually accept the repeated matches, introduce a trait, use another enum-dispatch pattern, or have you run into cases where neither option felt particularly satisfying?