[Solved] Problem with variable names in a macro


#1

I have a enum where each variant contains some data and some metadata. The metadata is always of a different type, but they all share a common trait. I’d like to lift this trait to the enum type.

Because I don’t want to write the same code over and over again (the trait has a lot of methods), here is a macro that helps me:

…

( $name:ident : $t1:ty ) => {
    fn $name(self, arg1: $t1) -> Self {
        match self {
            Graph::XY(data, meta) => Graph::XY(data, meta.$name(arg1)),
            Graph::Point(data, meta) => Graph::Point(data, meta.$name(arg1)),
        }
    }
};

( $name:ident : $t1:ty, $t2:ty ) => { ... }
…

All of the trait methods take ownership of self, some data and return ownership. This is some kind of builder pattern I guess.

I would like to avoid having all these different macro rules for different numbers of parameters. Idealy, there should only be

( $name:ident : $($t:ty),* ) => { ??? }

but I fail at unrolling that into a parameter list

fn $name(self, (???: $t)*) { 
    … meta.$name( (???)* ) …
}
  • Can anyone help me with that?
  • Am I maybe on the wrong track and can do this without any macro use?

#2

Could you give us more details on what kinds of parameters you expect to find into the macro? Like an example of what the code you want to convert into a macro looks like? It sounds like you’d want something like a recursive macro here though


#3

Creating a new symbol out of nowhere is called a gensym and still in the discussion. However my gut feeling is that you can easily work around the lack of gensym by having a fixed and long enough list of unique symbols. This is done in two phases, the first is to interleave $ts with predefined list of names:

macro_rules! interleave_and_call {
    ($next:ident! [$($acc:tt)*] [] $_nn:tt) => (
        $next!($($acc)*)
    );
    ($next:ident! [$($acc:tt)*] [$t:ty, $($tt:tt)*] [$n:ident, $($nn:tt)*]) => (
        interleave_and_call!($next! [$($acc)* ($n: $t)] [$($tt)*] [$($nn)*])
    );
}

This macro will translate interleave_and_call!(foo! [whatever here] [t1, t2,] [n1, n2, n3, ...]) into foo!(whatever here (t1: n1) (t2: n2)). Note that n3 and others are ignored. Also note that this is a recursive macro so it cannot handle too many arguments, but practically this should not be a problem unless you are using it as a part of complex external macros.

Now we can make the actual function (adapting to your code should be simple) with this macro:

macro_rules! make_fn {
    (@internal $name:ident : $(($n:ident: $t:ty))*) => (
        fn $name($($n: $t),*) {
            println!("{:?}", ($($n),*));
        }
    );
    ($name:ident : $($t:ty),*) => (
        // here we use 8 names, but it is up to you to supply more
        interleave_and_call!(make_fn! [@internal $name :] [$($t,)*] [a, b, c, d, e, f, g, h])
    );
}

fn main() {
    make_fn!(foo : i32, String);
    foo(42, "hello".into());
}

(I recommend to read The Little Book of Rust Macros for this level of macro usages.)


#4

Thanks to macro hygiene, any literal identifier is a gensym.

macro_rules! interleave_and_call {
    ($next:ident! [$($acc:tt)*] []) => (
        $next!{$($acc)*}
    );
    ($next:ident! [$($acc:tt)*] [$t:ty, $($tt:tt)*]) => (
        // Declare an unbounded number of variables uniquely named "x".
        interleave_and_call!{$next! [$($acc)* (x: $t)] [$($tt)*]}
    );
}

macro_rules! make_fn {
    (@internal $name:ident : $(($n:ident: $t:ty))*) => (
        fn $name($($n: $t),*) {
            println!("{:?}", ($($n),*));
        }
    );
    ($name:ident : $($t:ty),*) => (
        // here we use 0 names :P
        interleave_and_call!{make_fn! [@internal $name :] [$($t,)*]}
    );
}

fn main() {
    make_fn!(foo : i32, String);
    foo(42, "hello".into());
}

Though, granted, foo(x: i32, x: String) probably doesn’t make the best signature for anything that is going to show up in rustdoc.


#5

@lifthrasiir, @ExpHP: Thanks, both of you. It works great now!