Macro to impl multple fns for multiple structs?

I am trying to do something like this:

// macro definition
#[macro_export]
macro_rules! impl_multi {
    ($($types:ty,)+ { $($defs:tt)* }) => {
        $(impl $types {
            $($defs)*
        })+
    };
}

// usage
impl_multi!{Apples, Oranges, {
    fn foo(&self) {
        // I happen to know that Apples and Oranges both
        // have a storage() method
        let _storage = self.storage();
    }

    fn bar(&self) {
        let _storage = self.storage();
    }
}}

However, I get an error:

error: meta-variable types repeats 2 times, but defs repeats 8 times

Any ideas on how to make this work?

I realize this can be solved with traits, but in this case I don't want to have to define the functions twice.

If you're okay with changing the syntax a little, here's one option where the block for the impl is treated as one tt:

#[macro_export]
macro_rules! impl_multi {
    ($($types:ty),+ | $defs:tt) => {
        $(impl $types $defs)*
    };
}

impl_multi! {Apples, Oranges | {
    fn foo(&self) {
        let _storage = self.storage();
    }

    fn bar(&self) {
        let _storage = self.storage();
    }
}}

Someone with more macro experience might be able to come up with a cleaner solution.

2 Likes

(aside: don't #[macro_export] a helper / internal macro. Instead, append a pub(crate) use macro_name; after the macro definition)


So yeah, macro_rules! repetitions were designed to favor "zipping" over "flat_mapping" / combining / performing the cartesian product, even in cases where there would be no ambiguity. So, sadly, you can't directly combine those together. The simpler solution here would be to, as @Helioza suggested, use $:tt to capture the whole { $($:tt)* }, at which point you "just" have to tackle the ambiguity (w.r.t. that $:tt potentially being part of a $:ty), which Helioza tackled with |.

For the sake of generality, here is how you'd solve the pattern without changing your input syntax.

  1. Use recursion instead of repetition for one of these two repetitions. That way, for each step of the recursion, there won't be a repetition for that element, allowing you to combine them:

      macro_rules! impl_multi {
          (
    +         $first_ty:ty,
              $($types:ty ,)*
              {
                  $($defs:tt)*
              }
          ) => (
    -         $( impl $types { $($defs)* } )*
    +         impl $first_ty { $($defs)* }
    +         $crate::impl_multi! {
    +             $($types ,)*
    +             {
    +                 $($defs)*
    +             }
    +         }
          );
    
    +     (
    +         /* nothing left */
    +         { $($defs:tt)* }
    +     ) => ();
      } pub(in crate) use impl_multi;
    
  2. This can be quite nice for simple situations such as this one, where one of the sequences / repetitions is small.

    There is a more general technique, which generalizes quite well to more-than-2 sequences/repetitions, and which can be more performant if all the repetitions are big. It's kind of overkill for this situation, though, but I'll try to show it nonetheless for the sake of the example:

    // macro definition
    macro_rules! impl_multi {
        (
            $($each_type:ty ,)*
            {
                $($each_tt_def:tt)*
            }
        ) => (
            $crate::impl_multi! { @combine_tt_wrapped
                ( $($each_type)* )
                ( $($each_tt_def)* ) // as $tts_def
            }
        );
        
        (@combine_tt_wrapped
            ( $($each_type:tt)* )
            $tts_def:tt
        ) => (
            // here we can combine!
            $crate::impl_multi! { @unwrap
                $(
                    $each_type
                    $tts_def
                )*
            }
        );
        
        (@unwrap
            $(
                $each_type:tt ( $($each_tt_def)* )
            )*
        ) => (
            $(
                impl $each_type { $($each_tt_def)* }
            )*
        );
    } pub(in crate) use impl_multi;
    
    • Playground

    • A nifty trick for this pattern is to (ab)use the fact that high level captures such as :ty or :expr, when emitted, are already wrapped within "invisible parenthesis" which allows them to be parsed, afterwards, as one simple :tt each.

3 Likes

:exploding_head:

1 Like

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.