DEV Community

Cover image for Rust Trait Implementation Wizardry 🧙: Unveiling the Magic of Macro Rules
Liam Clegg
Liam Clegg

Posted on • Edited on • Originally published at liamclegg.co.uk

Rust Trait Implementation Wizardry 🧙: Unveiling the Magic of Macro Rules

IMPORTANT: This article will be a continuation of my previous article about rust traits. So go quickly read it first if you need to understand rust traits. The best TRAIT of RUST 🔥 (no pun intended)

Introduction

If you have ever used rust it may seem like you have to rewrite a lot of code due to how the only way you can add repeatable functionality is through composing your structs with traits. Well lucky for you there is this amazing feature called macros.

What is a macro in rust?

Macros can help use write a function that generates code at compile time. Using macros can make you a much faster programmer since you can write a macro that will implement traits for you meaning you don't have to keep doing the tedious task of rewriting your implementation over and over again.

There are 2 main types of macros in rust: declarative and procedural. Declarative macros in rust are usually referred to as macro rules since they are created but typing macro_rules!. On the other hand procedural macros are used for directly accessing the token stream to consume and produce syntax in rust. Procedural macros are used through the proc-macro crate and are not going to be the focus of this article.

For this article we will be using macro rules since they are builtin without having to add any crates and are much simpler to create.

Let's start coding!

For this example we will be continuing from the last article about rust traits. If you wish to follow along you can grab the code from GitHub: Best trait of rust repository

What is a number?

So far we have said that to create a Vec2 for type T it must implement the Add trait. This is so that Vec2 can be also used for addition. Since we need lots more operations we can define a number trait to make things a little easier.

pub trait Number: 
    Clone +
    Copy +
    Sized + 
    Add<Output = Self> + 
    Sub<Output = Self> + 
    Div<Output = Self> + 
    Mul<Output = Self> + 
    AddAssign + 
    SubAssign + 
    DivAssign + 
    MulAssign {}
Enter fullscreen mode Exit fullscreen mode

This trait might not have any direct functionality but it does mean when ever we want to say that a generic has all the other necessary trait we just have to say it implements number.

This now means that we will need to implement number for all the number types. The first thought might be to write it out like this.

impl Number for f32 {}
impl Number for f64 {}
impl Number for u32 {}
impl Number for u64 {}
impl Number for u128 {}
impl Number for i32 {}
impl Number for i64 {}
impl Number for i128 {}
Enter fullscreen mode Exit fullscreen mode

Now this isn't to bad and it might be the way you would prefer to write your code since it is more verbose. But for the sake of this article we will make this less verbose through macro rules.

First we think of how we wish the macro rule to look. To make the code super simple we can have a macro rule named impl_number with a list of types that will implement number. It will look something like this.

impl_number!(usize, f32, f64, u32, u64, u128, i32, i64, i128);
Enter fullscreen mode Exit fullscreen mode

To create a macro we start with macro_rules! followed by the name of the macro impl_number.

macro_rules! impl_number {
    // code goes here
}
Enter fullscreen mode Exit fullscreen mode

We now need need to actually create a the code to generate the line impl Number for type_name_here {}.

To do this we create a function that takes the designator ty (which is used for types). A designator is basically just the type of thing you are passing to the macro. The matcher will be written as $t:ty where $t is the argument name and ty is the designator type.

We can then simply write in the function the code we wish to generate. So in this case impl Number for $t {}. To impliment a single type for a trait would look like this.

macro_rules! impl_number {
    ($t:ty) => {
        impl Number for $t {}
    };
}
Enter fullscreen mode Exit fullscreen mode

So this is great and all but this only implements one type. To allow a list of arguments to be passed we need surround the matcher with $(...),+. If we wish to repeat the generated code for each argument we can surround the code with $(...)*. All together this will look like the following.

macro_rules! impl_number {
    ($($t:ty),+) => {
        $(impl Number for $t {})*
    };
}
Enter fullscreen mode Exit fullscreen mode

We can now below this write the following to actually implement the types.

impl_number!(usize, f32, f64, u32, u64, u128, i32, i64, i128);
Enter fullscreen mode Exit fullscreen mode

Now we can rewrite the Vec2 struct from this:


pub struct Vec2<T> 
where
    T: Add<Output = T> + Copy + Clone
{
    pub x: T,
    pub y: T
}
Enter fullscreen mode Exit fullscreen mode

To this:

#[derive(Debug, Copy, Clone)]
pub struct Vec2<T: Number>  {
    pub x: T,
    pub y: T
}
Enter fullscreen mode Exit fullscreen mode

Operations

There is mainly 2 operations we wish to implement for the Vec2 struct. The first is operators that create a new value and then there's operators that also assign the value. In the previous article we looked at how the implement the Add trait and used the following code:

impl<T> Add for Vec2<T> 
where
    T: Add<Output = T> + Copy + Clone
{
    type Output = Vec2<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Vec2 {
            x: self.x + rhs.x, 
            y: self.y + rhs.y
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If we look at this implementation block there are only a couple things that change between operators. The trait name changes and the function name changes. Although the operator symbol also changes we can just call the method directly instead.

To implement this as a macro we will use the name impl_vec2_op. This will take 2 designators: an identifier for the trait and an identifier for the method name. An identifier is represented by the keyword ident. We can then replace the trait name with $trait and the method name with $func. The completed macro rule looks like the following.

macro_rules! impl_vec2_op {
    ($trait:ident, $func:ident) => {
        impl<T: Number> $trait for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: Self) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs.x),
                    y: self.y.$func(rhs.y)
                }

            }
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

We then run the macro with add, sub, div and mul with the following lines.

impl_vec2_op!(Add, add);
impl_vec2_op!(Sub, sub);
impl_vec2_op!(Div, div);
impl_vec2_op!(Mul, mul);
Enter fullscreen mode Exit fullscreen mode

To show the benefits to this macro based approach lets say we now wish to allow adding a number T to a vector Vec2<T>. We can easily add the implementation block to the macro so that it is implemented for all the operations.

impl<T:Number> $trait<T> for Vec2<T> {
    type Output = Self;

    fn $func(self, rhs: T) -> Self::Output {
        Vec2 {
            x: self.x.$func(rhs),
            y: self.y.$func(rhs)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Altogether the macro would look like this:

macro_rules! impl_vec2_op {
    ($trait:ident, $func:ident) => {
        impl<T: Number> $trait for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: Self) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs.x),
                    y: self.y.$func(rhs.y)
                }
            }
        }

        impl<T:Number> $trait<T> for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: T) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs),
                    y: self.y.$func(rhs)
                }
            }
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

We can now similarly do this for the assignment operators with the following macro rule.

macro_rules! impl_vec2_op_assign {
    ($trait:ident, $func:ident) => {        
        impl<T: Number> $trait for Vec2<T> {
            fn $func(&mut self, rhs: Self) {
                self.x.$func(rhs.x);
                self.y.$func(rhs.y);
            }
        }

        impl<T: Number> $trait<T> for Vec2<T> {
            fn $func(&mut self, rhs: T) {
                self.x.$func(rhs);
                self.y.$func(rhs);
            }
        }
    };
}

impl_vec2_op_assign!(AddAssign, add_assign);
impl_vec2_op_assign!(SubAssign, sub_assign);
impl_vec2_op_assign!(DivAssign, div_assign);
impl_vec2_op_assign!(MulAssign, mul_assign);
Enter fullscreen mode Exit fullscreen mode

Finally we can update our ToVec2 trait. For this we will instead use the From trait since it is included in the standard library. Since we have a generic we do not need to create a macro rule for this.

impl<T: Number> From<T> for Vec2<T> {
    fn from(val: T) -> Self {
        Vec2 {
            x: val,
            y: val,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Lets quickly write a test

We first need to implement partial equals for the Vec2 struct. This is simple. We just need to add it to number and then derive it in Vec2.

pub trait Number: 
    PartialEq +
    ...
Enter fullscreen mode Exit fullscreen mode
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vec2<T: Number>  {
    ...
Enter fullscreen mode Exit fullscreen mode

Note: I removed the main.rs file from the previous article

We can then write our test in lib.rs as follows:

pub mod vec2;

#[cfg(test)]
mod tests {
    use crate::vec2::Vec2;

    #[test]
    fn it_works() {
        // 5, 5
        let v1: Vec2<i32> = 5.into();

        // 1, 2
        let v2 = Vec2 {
            x: 1,
            y: 2
        };

        // 6, 7
        let mut v3 = v1 + v2;

        // 12, 14
        v3 *= Vec2::from(2);

        assert_eq!(v3, Vec2 {
            x: 12,
            y: 14
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

We can now run the test with:

cargo test
Enter fullscreen mode Exit fullscreen mode

And we get the following output.

The terminal output for the test

IT WORKS!!!

Find the full code here: Trait Implementation Wizardry

Challenge

Try and write some macros for casting between numbers using the from trait.

Conclusion

We have explored how to fix the issue with repeating trait implementation blocks by using macro rules and how we can practically do this with the Vec2 example from the previous article. If you have any questions leave a comment and it would be much appreciated if you drop a like if you enjoyed this article, thanks!

Top comments (0)