Can I add lifetime and extra types for macro that implements trait?

I did this simple struct to automatically implement a trait for a struct, and you even can pass the data name to it, as well as the struct name. However, what if A had more types and lifetime bounds than BorrowSomething?

struct A<'a, T> {
    data: &'a[T]
}

trait BorrowSomething<'a, T>{
    fn borrow_something(&self) -> &'a[T];
}

macro_rules! impl_borrow {
    ($data_name:ident, $struct_name:ident) => {
        impl<'a, T> BorrowSomething<'a, T> for $struct_name<'a, T> {
            fn borrow_something(&self) -> &'a[T] {
                self.$data_name
            }
        }
    };
}

impl_borrow!(data, A);

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b479c1faab9e287641a1313131f6ac4f

Example: what if I wanted to implement BorrowSomething for

struct A<'a, 'b, T, I, U> {
    //...
}

?

Can I add them to the macro?

Order matters for generics, not the names in the definition. You could have used 'b and U in your macro for example, and you would still use the first (only) lifetime and type generic positions.

So you're going to have to convey to the macro which generic lifetimes and lifetimes are being implemented and which don't matter to the implementation. Here's one way for your example; it makes some assumptions like "at least one lifetime" so it's not the most entirely robust.

struct A<'a, 'b, T, U> {
    data: &'a [T],
    other: &'b U,
}

trait BorrowSomething<'a, T> {
    fn borrow_something(&self) -> &'a[T];
}

macro_rules! impl_borrow {
    ([$($impl_generics:tt)*],
     $a:lifetime,
     $T:ident,
     $data_name:ident,
     $ty:ty) => {
        impl<$($impl_generics)*> BorrowSomething<$a, $T> for $ty {
            fn borrow_something(&self) -> &$a[$T] {
                self.$data_name
            }
        }
    };
}

impl_borrow!(['a, 'b, T, U], 'a, T, data, A<'a, 'b, T, U>);

Here:

  • I replaced $struct_name with a :ty matcher to let an arbitrary type be written.
  • Because the user will be writing the entire type, it's possible that they might want to call 'a and T different things. So I also let the user specify 'a and T used in BorrowSomething. This is just following the general rule of thumb of, "if a user names something in one place, they should name it in all places".
    • For types it doesn't matter, but for some other places like expressions this is actually required due to macro hygiene!
  • For generics, I used a $()* to match a sequence of things. Generics are somewhat hard to match, so I used the all-powerful :tt (token tree) to match arbitrary tokens, which can be pasted straight into the impl<...>.
  • Because $($x:tt)* can match anything, I further wrapped these tokens inside a [] so the macro knows where they end.
    • <> might seem more intuitive, but unfortunately they cannot be parsed as a single token tree since they are also operators. Only [], {}, and () work for this purpose.
2 Likes

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.