An enum whose variants are collected from macros scattered in codes?

Hi,

The title is basically kind of an XY Problem, but I found both X and Y are interesting to discuss about so I took Y as the title, please do not blame me. :smile:

My original question came from the use of enum_dispatch. Its example code is:

struct MyImplementorA;
struct MyImplementorB;
impl MyBehavior for MyImplementorA { /*...*/ }
impl MyBehavior for MyImplementorB { /*...*/ }

#[enum_dispatch]
enum MyBehaviorEnum {
    MyImplementorA,
    MyImplementorB,
}

#[enum_dispatch(MyBehaviorEnum)]
trait MyBehavior {
    fn my_trait_method(&self);
}

let a: MyBehaviorEnum = MyImplementorA::new().into();

a.my_trait_method();    // no dynamic dispatch

I found the definition of MyBehaviorEnum to be a bit cumbersome. Every time we create a new struct that implements MyBehavior, we must manually add it to the enum definition. If we decide to rename or merge similar structs while refactoring, it will require double changes which complicates project maintenance. Additionally, as more implementers are added, the enum definition becomes lengthier too.

So what if we could write the structs only once, and let the library generate the rest (i.e. the enums)? I imagine something simpler like:

#[enum_dispatch(MyBehaviorEnum)] // send MyImplementorA to MyBehaviorEnum
struct MyImplementorA;
#[enum_dispatch(MyBehaviorEnum)] // send MyImplementorB to MyBehaviorEnum
struct MyImplementorB;

#[enum_dispatch]
enum MyBehaviorEnum {
    /* auto generated */
}

#[enum_dispatch(MyBehaviorEnum)]
trait MyBehavior {
    fn my_trait_method(&self);
}

Now I am wondering about how feasible it is. Rust seems not to provide procedure macros with a way of gathering global states, but a crate called macro_state manages to accomplish it by writing temporary files during macro running.


But it looks like there are still some gaps to implement these with enum_dispatch. I have heard that Rust macros are "hygienic" so (maybe) it is hard to interact with macros in another crate.

In fact I had some initial attempts by modifying macro_state to add a macro called #[auto_enum("key")], which collects all values from append_auto_enum!(key, value) to create an enum:

mod A { append_auto_enum!("foo", MyImplementorA); }
mod B { append_auto_enum!("foo", MyImplementorB); }

#[enum_dispatch]
#[auto_enum("foo")] // --> will expand to `enum MyBehaviorEnum { MyImplementorA, MyImplementorB }`
enum MyBehaviorEnum {}

// MyBehaviorEnum::MyImplementorA is available here!

However, enum_dispatch complains that it does not find any variant of MyBehaviorEnum, thus generate non-exhaustive match patterns:

error[E0004]: non-exhaustive patterns: type `&MyBehaviorEnum` is non-empty
  --> src/main.rs:96:5
   |
96 |     #[enum_dispatch]
   |     ^^^^^^^^^^^^^^^^
   |
note: `MyBehaviorEnum` defined here
  --> src/main.rs:98:14
   |
98 |     pub enum MyBehaviorEnum{}
   |              ^^^^^^^^^^^^^^^^^
   = note: the matched value is of type `&MyBehaviorEnum`
   = note: this error originates in the attribute macro `enum_dispatch` (in Nightly builds, run with -Z macro-backtrace for more info)
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown
   |
96 ~     #[enum_dispatch] {
97 +         _ => todo!(),
98 +     }
   |

So maybe there are some better methods to achieve it? Maybe I have to modify enum_dispatch itself? Maybe "having something auto-generated from different part of codes" is just a bad idea? ... Any suggestion or discussion is welcomed.

Disclaimer: I am not a native English speaker. Please correct me if there is anything mistaken or confusing in my post.

If you find yourself adding more and more variants (especially unrelated ones), you probably want a trait object, not an enum.

4 Likes

I agree what @H2CO3 said, probably it’s better using trait objects.

But, it’s interesting to find how far we can go with the help of proc_macros and build scripts. I wrote a crate named inwelling, which does helping to gather global states. You may have a try if it’s really worth the effects.

Macros are expanded in order, you need to put yours on top of enum_dispatch and you'll need to expand to an enum that's still annotated with enum_dispatch

Yes this can be a short answer to my question. But for some reasons this solution could be out of my consideration at all:

  • enum_dispatch can be >10x faster than dynamic dispatch. With current implementation of dynamic dispatch, this performance difference is just hard to ignore in some cases where Vecs storing multi-type values are heavily used, e.g. in loops.
  • Enumerations have an important bonus over trait objects: they are enumerable. This means that it can be used with some crates (e.g. EnumIter in strum) to achieve something that is difficult or impossible with traits: for example, enumerating all the types that implement some trait. That could be useful in some scenarios: initialize all types that implement a specific trait, or automatically generate API documentation based on the structs with the same trait (e.g. utoipa's way of doc declaration).

Thanks! I will check that.

I was thinking that Rust's macros are expanded from the bottom up (i.e. enum_dispatch!(auto_enum!(MyBehaviorEnum))), thanks for your reminder. I change the order now, but the compiler message is not getting better:

error: custom attribute panicked
  --> src/main.rs:98:5
   |
98 |     #[enum_dispatch]
   |     ^^^^^^^^^^^^^^^^
   |
   = help: message: Named enum_dispatch variants must have one unnamed field

I think there's some logic error in your macro which results in outputting an enum variant without field, which then enum_dispatch complains about. You could try temporarily removing the #[enum_dispatch] attribute and use cargo expand to inspect the output of your macro.