How to create a macro to impl a provided type-parametrized trait?


#1

The macro below impls the provided trait. It must take its trait parameter as an ident for the compiler to allow one to write <T: $trait_>. Neither ty nor tt seem to suffice. Unfortunately, this macro fails to accept a templatized trait, and I am a bit at a loss on how to continue. Any workarounds?

macro_rules! foo {
    ($trait_:ident) => {
        impl $trait_ {
            pub fn bar<T: $trait_>(&self) {}
        }
    }
}

trait Trait1 {}
trait Trait2<T> {}

foo!(Trait1);
//foo!(Trait2<T>);  // error: no rules expected the token `<`

fn main() {}

Playground


#2

Generics, are, in general, impossible to support in macros. Blame it on the lifetimes.

In specific cases, however, you can overload the rules to support common “shapes”:

macro_rules! foo {
    ($trait_:ident) => { foo! { $trait_<> } };
    ($trait_:ident < $($args:ident),* $(,)* >) => { ... };
}

Of course, this fails to take constraints into account. So you try to do that as well, and you end up trapped in a pit of special cases that only kinda-sorta work and are all trivially broken by some bastard trying to put a lifetime in there and then you remember that there are where clauses and by this point you’re wearing your pants on your head to keep the lifetime goblins out and babbling to yourself in some unknown language that…

*is wheeled away on a gurney*


#3

That…was not an expected solution. :wink:

I am still having trouble getting things compiling, however. For example,

macro_rules! foo {
    ($trait_:ident) => { foo! { $trait_<> } };
    ($trait_:ident < $($args:ident),* $(,)* >) => {
        impl $trait_ {
            pub fn bar<T: $trait_<$($args)*>>(&self) {}
        }
    };
}

trait Trait1 {}
trait Trait2<T> {}

foo!(Trait1);
foo!(Trait2<T>);  // error: no rules expected the token `<`

fn main() {}

(Playground)

gives error: local ambiguity: multiple parsing options: built-in NTs ident ('args') or 1 other option. on the $trait_<> whereas, just making a template-only version (i.e. splitting these into two macros foo and fooT) still gives me the same error as before on fooT!(Trait2<T>);.


#4
macro_rules! foo {
    (@$trait_:ident [$($args:ident,)*]) => {
        impl<$($args),*> $trait_<$($args),*> {
            #[allow(non_camel_case_types, dead_code)]
            pub fn bar<__foo_T: $trait_<$($args),*>>(&self) {}
        }
    };
    
    ($trait_:ident <>) => { foo! { @$trait_ [] } };
    ($trait_:ident < $($args:ident),* $(,)* >) => { foo! { @$trait_ [$($args,)*] } };
    ($trait_:ident) => { foo! { @$trait_ [] } };
}

trait Trait1 {}
trait Trait2<T> {}

foo!(Trait1);
foo!(Trait2<T>);

fn main() {}

#5

Interesting if PhantomData could not help with this somehow…


#6

How would it? Types don’t interact with macro expansion in any way whatsoever.


#7

Thanks, Daniel, this is extremely useful! Can you comment on the following:

  1. In < $($args:ident),* $(,)* >, why is the extra $(,)* there? It doesn’t seem to help capture a trailing comma (I get an error if I put one in there).
  2. Why did it need to translate <> to []? Or was that Was it only to strip out the extra $(,)*? In particular, why is there a parsing ambiguity when I replace [] in your solution with <>?
    (I couldn’t find anything via some Google searches through your book https://danielkeep.github.io/tlborm/book/, though I might have missed something.)

#8
  1. Well, it’s supposed to. It’s probably because <...> doesn’t count as a delimited TT and urgh I hate generics.

  2. Because <...> doesn’t count as a delimited TT. Basically, macro_rules! is really stupid, and you need to give it all the help you can. [...] is a safer bet, and once I needed to start normalising the syntax to deal with empty/non-empty parameter lists, it was easier to use that.

To be honest, I don’t really even know how or why half the things in macro_rules! work the way they do. Kinda like my grasp of English—I don’t know why I put an em-dash there, it just felt right.


#9

Sometimes em-dashes are meant to be…

In any case, perhaps this needs to be publicized a bit more to library writers providing macros that take traits for arguments. I just added it to one of my crates thanks to you and thanks to me actually needing it.

The current solution is still limited in other ways – it doesn’t support traits with associated types: Trait<Associated=Type>.


#10

Actually, one more follow-up. Is there a DRY way to put in a where clause only when there is at least one type parameter? Specifically, the where clause below fails when there are no type parameters. My workaround was to make a separate (@$trait_:ident) case that lacked the where clause, but I had to replicate the implementation body.

    (@$trait_:ident [$($args:ident,)*]) => {
        impl<$($args),*> $trait_<$($args),*>
            where $( $args: ::std::marker::Copy ),*
        {
            #[allow(non_camel_case_types, dead_code)]
            pub fn bar<__foo_T: $trait_<$($args),*>>(&self) {}
        }
    };
    
    ($trait_:ident <>) => { foo! { @$trait_ [] } };
    ($trait_:ident < $($args:ident),* $(,)* >) => { foo! { @$trait_ [$($args,)*] } };
    ($trait_:ident) => { foo! { @$trait_ [] } };
}

#11

Did I mention I hate generics? I should have mentioned: I also hate where clauses. No, there isn’t, and yes, it’s invalid to have where without any predicates after it, and there’s no “inert” predicate you can always insert.

The best solution I’ve found is an intermediate macro that potentially pastes a where clause between two arbitrary chunks of tokens, provided it’s not empty.


#12

Your example is very interesting – it allows user-specified where clauses, which I also happen to need. But I would like to avoid making an all-out TT-muncher like you did, at least for my initial attempt. So I’d like to focus on a simple example.

Say, I want to add my own constraint of Any + 'static on all the trait type parameters, and merge these with user-specified where clauses. I reproduce here only that subset of the full macro.

macro_rules! foo {
    (@$trait_:ident [$($args:ident,)*] where [$($preds:tt)+]) => {
        impl<$($args),*> $trait_<$($args),*>
            where $($args: ::std::any::Any + 'static,)*
                  $($preds)*
        {
            #[allow(non_camel_case_types, dead_code)]
            pub fn bar<__foo_T: $trait_<$($args),*>>(&self) {}
        }
    };

    (
        $trait_:ident < $($args:ident),* $(,)* >
        where $($preds:tt)+
    ) => {
        foo! { @$trait_ [$($args,)*] where [$($preds)*] }
    };
}

trait Trait<T: Copy> {}

foo!(Trait<T> where T: Copy);

fn main() {}

Playground

I get

<anon>:5:21: 5:27 error: expected type, found `T`
<anon>:5                   $($preds)*
                             ^~~~~~

I read through the relevant parts of the second chapter of your macro book again. I don’t quite understand why it is expecting a “type” here rather than treating $($preds)* as a sequence of tokens that it will parse at a later stage. (Also, I would presume it’s treating the two Ts in the macro invocation as the same symbol, if it was looking for an ident, at least.) Any insights?


#13

Basically, as soon as you have $($stuff)* somewhere, and stuff was captured as tt, you need to use the reparse trick/ast coercion. The parser isn’t smart enough to “unpack” the bundle of tts, so you put them inside another macro invocation, which causes them to be unpacked and reparsed. In this case, just wrap the whole expansion in an as_item invocation.


#14

Thanks a lot Daniel! I’ve learned a lot with this exercise! Here’s the solution for any others following.

Also I have updated downcast-rs to take advantage of this technique as I have need of this.