Metaprogramming: Define a set of items once, and loop over them at compile time

I have a set of items that I want to execute the same code for in different places in my codebase.
Let's say they are methods for my Lua scripting API:

trait Method {
    /// Name of the method
    const NAME: &'static str;
    /// Help text for the method
    const HELP: &'static str;
    /// Arguments the method takes when called
    type Args;
    /// Return type
    type Ret: IntoLuaMulti<'static>;
    /// The function that gets called
    fn call<'a>(lua: &Lua, exec: &mut LuaExecContext, args: Self::Args) -> mlua::Result<Self::Ret>;
}

Now I create types for each method that I want to define, and implement this trait for each. For the sake of brevity, let's just name the types Method1, Method2, Method3, etc....

Now, at one place in my code I need to add the methods to my Lua user type. It looks like this for a single item:
methods.add_method_mut(Method1::NAME, Method1::call);

I don't want to do this manually for every single type. I found a crate called forr that offers a nice macro that lets me generate code for each item.

        forr::forr! {$t:ty in [Method1, Method2, Method3, Method4, Method5, Method6, ...] $* {
            methods.add_method_mut($t::NAME, $t::call);
        }};

Problem solved, right? Well, partially.

Now I want to add a help section somewhere else that lists each method, and shows help for them.

        forr::forr! {$t:ty in [Method1, Method2, Method3, Method4, Method5, Method6, ...] $* {
            ui.label($t::NAME);
            ui.label($t::HELP);
        }};

Notice how I have to repeat all the items.

What if I stored them in a macro?

macro_rules! methods {
    () => {
        [Method1, Method2, Method3, Method4, Method5, Method6, ...]
    };
}
...
        forr::forr! {$t:ty in methods!() $* {
            methods.add_method_mut($t::NAME, $t::call);
        }};
error: unexpected token: expected `[...]`    

Nope, that doesn't work, because the macro isn't eagerly evaluated by the procedural macro, so I can't do this.

Is there a way to avoid having to repeat myself?

You could bypass the forr sugar and do "macro dispatch" directly, e.g.

// dispatch macro
macro_rules! for_methods {($m:ident) => {
    $m!(Method1);
    $m!(Method2);
    // …
}}

// usage
macro_rules! add_method_mut {($t:ty) => {
    methods.add_method_mut($t::NAME, $t::call);
}}
for_methods!(add_method_mut);

If you want to utilize forr, you need to write an "outer" macro instead of an "inner" one. This is a bit difficult due to the indirect methods currently required to emit literal $ in a macro expansion, but can be fairly simple once you know the trick:

// dispatch macro
macro_rules! define_for_methods {($_:tt) => {
    macro_rules! for_methods {
        {$_ $fragment:ident : $kind:ident in $_ * {
            $($tt:tt)*
        }} => {
            forr! {$_ $fragment : $kind in [Method1, Method2, …] $_ * {
                $($tt)*
            }
        };
    }
}}
define_for_methods!($);

// usage
for_methods! {$t:ty in $* {
    methods.add_method_mut($t::NAME, $t::call);
}}

Here we're defining a $_ fragment that expands to $. This is the trick behind unlocking all sorts of macro generated macro tricks. In fact, you can get forr-like syntax without any proc macros by utilizing a "macro continuation" pattern:

// dispatch macro
macro_rules! for_methods {($($tt:tt)*) => {
    macro_rules! __invoke_macro_continuation {
        $($tt)*
    }
    __invoke_macro_continuation!(Method1);
    __invoke_macro_continuation!(Method2);
    // …
}}

// usage
for_methods! {($t:ty) => {
    methods.add_method_mut($t::NAME, $t::call);
}}

This one is actually simple enough to not need an escaped dollar.

However, these are all advanced tricks which make your code more difficult to understand if you aren't already familiar with the tricks/macros in use. You should seriously consider whether a simpler solution would be sufficient; often times a bit of duplication is fine, or can be avoided in a different way.

For example, I'd consider a fn register<M: Method> which does both the ui.label(M::HELP) and methods.add_method_mut(M::NAME, M::call), registration, and now you only need to write the list once when calling register. (Although this does assume retained-mode instead of immediate-mode UI registration that can happen up front.) Or if you fall down to an object-safe level, you could have Vec<&'static dyn MethodShim>. (This relies on being able to directly use the dynamic layer under the more convenient types layer used by add_methods_mut. You can even implement the object-safe MethodShim in terms of the more convenient Method trait.)

1 Like

Thank you for the detailed answer!

I considered each solution, and while none of them seem too attractive, the first one seems the least complicated, so I went with that.

I needed to replace $t::FOO with <$t>::FOO for some reason, but other than that, the change was fairly easy.

It was a nice bonus that I could remove the forr dependency with the first solution.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.