Search code examples
rustoptimizationmacrostuples

How to Access Tuple Values Inside Enum Variants Defined Through a Rust Macro


I've defined a set of enums for different categories of weapons in my Rust application using a macro. Each enum variant is associated with a tuple containing values for weight, damage, and value. Here's the macro and its usage:

macro_rules! define_category_enum {
    (
        $trait_name:ident { $( $field_name:ident : $field_ty:ty ),* };
        $($category:ident {
            $($variant:ident 
                ($( $weight_val:expr, $damage_val:expr, $value_val:expr ),* )
            ),* $(,)?
        } );* $(;)?
    ) => {
        pub trait $trait_name {
            $(fn $field_name(&self) -> $field_ty;)*
        }

        $(#[derive(Debug, Clone, PartialEq)]
        pub enum $category {
            $($variant),*
        })*
    };
}

define_category_enum! {
    Weapon { weight: f32, damage: i32, value: i32 };
        Melee {
            Sword   (3.4, 50, 20),
            Axe     (5.0, 60, 25),
            Hammer  (6.5, 70, 30),
            Dagger  (1.0, 30, 15),
            Spear   (4.0, 55, 22),
        };
        Shield {
            Medium (4.5, 40, 10),
            Great  (7.5, 80, 12),
        };
        Range {
            Bow      (2.5, 45, 18),
            Crossbow (3.5, 60, 23),
            Gun      (5.0, 75, 28),
            Scepter  (2.0, 35, 16),
        };
}

My goal is to access and use the tuple values (weight, damage, value) associated with each enum variant programmatically, but I'm unsure how to proceed after defining the enums using this macro. I understand how to match against enum variants, but accessing the tuple values within those variants in a generic or trait-based manner eludes me.

Questions:

  • How can I programmatically access the tuple values (weight, damage, value) contained within each enum variant?
  • Is there a way to implement methods for these enums (or through a trait) that would allow me to retrieve these tuple values for any given variant?
  • Any recommendations for structuring this kind of data or macro to facilitate easier access to these internal values?

Solution

  • What you ask is possible, but not especially practical within declarative macros. This is close to (or beyond) the point where I would switch to procedural macros. The problem is that you have multiple sequences (variants, fields, ...) that don't line up one-to-one, which means you need to do this "manually", rather than rely on the macro repetition. Here is one possible solution:

    macro_rules! define_category_enum {
        (
            $trait_name:ident { $( $field_name:ident : $field_ty:ty ),* };
            $($category:ident {
                $($variant:ident ( $( $val:expr ),* ) ),* $(,)?
            } );* $(;)?
        ) => {
            define_category_enum!(@define_trait($trait_name { $( $field_name : $field_ty, )* };));
            define_category_enum!(@define_impls(
                $trait_name { $( $field_name : $field_ty, )* };
                { $($category {
                    $( $variant ( $( $val, )* ), )*
                }; )* }
            ));
        };
        (@define_trait(
            $trait_name:ident { $( $field_name:ident : $field_ty:ty, )* };
        )) => {
            pub trait $trait_name {
                $(fn $field_name(&self) -> $field_ty;)*
            }
        };
        (@define_impls(
            $trait_name:ident { $( $field_name:ident : $field_ty:ty, )* };
            {}
        )) => {
            // base case: no more impls, done
        };
        (@define_impls(
            $trait_name:ident { $( $field_name:ident : $field_ty:ty, )* };
            { $category:ident {
                $( $variant:ident ( $( $val:expr, )* ), )*
            }; $($rest:tt)* }
        )) => {
            // recursive case: emit one impl
            pub enum $category { $($variant,)* }
            impl $trait_name for $category { define_category_enum!(@define_fields(
                { $( $field_name : $field_ty, )* };
                { $($variant ( $( $val, )* ), )* }
            )); }
            // then recurse
            define_category_enum!(@define_impls(
                $trait_name { $( $field_name : $field_ty, )* };
                { $($rest)* }
            ));
        };
        (@define_fields(
            {};
            { $($variant:ident (), )* }
        )) => {
            // base case: no more fields, done
        };
        (@define_fields(
            { $field_name:ident : $field_ty:ty, $($rest:tt)* };
            { $($variant:ident ( $val:expr, $( $rest_val:expr, )* ), )* }
        )) => {
            // recursive case: emit one field
            fn $field_name(&self) -> $field_ty {
                match self {
                    $(Self::$variant => $val,)*
                }
            }
            // then recurse
            define_category_enum!(@define_fields(
                { $($rest)* };
                { $($variant ( $( $rest_val, )* ), )* }
            ));
        };
    }
    

    It is rather large, but a lot of it is boilerplate (and some parts could be shortened or made more elegant). The overall structure is this:

    • The first rule is the entrypoint. This parses the user input. We then "call" our macro again, but we enter different branches.
      • Here we use a trick to organise macros, using the @ symbol as a reserved token. Technically the user could write this in their macro call, but that does not matter too much. (There are other ways to do this, using nested macros.)
    • The second rule, @define_trait, will simply emit the trait with all its "field" functions. This is the same as your original version.
    • The third and fourth rule, @define_impls, take care of recursively building the category enums and their implementations of the trait.
      • Here we use another trick: rather than matching on the entire sequence of categories, we match on one, then match the rest as "anything" ($($rest:tt)*). We handle the one thing we matched, then delegate the rest to a recursive call of the macro. We need a base case (the third rule) to stop the recursion once there are no more categories.
    • The fifth and sixth rule, @define_fields, invoked from the previous, emits the functions within the impl blocks. The recursion here is similar to the previous rule, but we are simultaneously taking elements from the fields sequence and the variant's values sequence.