home  >  software  >  rusts-complexity  >  macros

Exploring Rust’s Complexity

Macros

Macros are very much a double edged sword. They hold a lot of power for programmers to make your code very compact and clever. But clever code is not always good and too much macro magic can lead to a lot of complexity and hidden behaviour.

So my advice is to enjoy macros with care. Do not use them to save 3 lines of code by making a very clever one liner. Use them to make your code more explicit and more understandable.

Also we will not cover procedural macros in this post, which are used via the derive keyword. They are their own beast and deserve a dedicated post.

Macros are invoked by calling the name of the macro and a !. You have probably already been using a couple of macros when learning the basics of rust. println! and vec! are very common macros and are found in almost all code.

At their core, macros allow you to modify the code’s AST (abstract syntax tree). So it is code that generates code. For example, the following vec! macro will expand to the following code.

// these are equivalent

// with macro
let n = vec![1, 2];

// with standard rust code
let n = {
    let mut tmp = Vec::new();
    tmp.push(1);
    tmp.push(2);
    tmp
};

You do not have to use [] by the way. You can alos use () or {}, your call.

So what the heck is going on here? The standard rust code just goes to show, what the vec! will look like to the compiler when it is expanded. If we take a look at the vec! macro, this is the implementation:

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

It looks very odd, but just bare with me. First, we define the name of our macro. vec, easy enough. Now we can have match statements. Here we only need one, but you can have as many as you want. So we are saying that we want to match any sequence (denoted by the *) of expressions (which are a part of any programming language syntax) that are separated by a ,. Now we tell the macro what to do with our code if it matches our pattern. And here we can already recognize the standard rust code. We tell the macro to create a temporary Vec (mutable, since we are going to push items to it). Now we call push for every $x expressions we encounter (again, denoted with the *) and in the last line we return our array (since it does not end with a ;). If you are confused by the $( ...)* line, just think of it as a for each loop.

So what else can we match. In theory, anything you want. Let us define a macro that simplifies making our lisp-style Lists, that you had to nest with Cons and Nils.

This is our list struct:

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil
}

We can use our list like this:

use crate::List::{Cons, Nil};

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil
}

fn main() {
    let lst = Cons(3, Box::new(Nil));
    println!("l: {:?}", lst);
}

But these statements can get very long very quickly. Imagine this:

let lst = Cons(3, Box::new(Cons(5, Box::new(Nil))));

Just to have 2 items in our list. We can do better - with macros. Let’s make a macro, similar to the vec! macro that will construct a list.

We should be able to use it like this:

let lst = construct!(3, 1, 5);

// this should print the following:
// lst: Cons(3, Cons(1, Cons(5, Nil)))
println!("lst: {:?}", lst);

Thankfully we already know how to match a sequence of comma seperated expressions. So let’s start with that.

macro_rules! construct {
    ($ ( $x:expr ),* ) => {
        Nil
    }
}

This will always return a Nil List. So how do we actually concatenate our values? Since we do not have a Vec that we can just push our items to, we will have to get a little more creative. Thankfully we can recursively expand our macro. If you ever used a functional programming language like haskell, erlang, or a lisp, you might recognize the “x:xs” syntax. It is used to split a list into its head and tail. So if we have a list with the following items (1, 2, 3). The head will be item 1 and the tail will be the remainding items (2, 3). So we want to recursively wrap our item into a Cons(item, *call macro again for tail*) structure. This might sound very complicated, but let’s just show the code, and hopefully that will clear things up. So to split up our expression, we can do the following:

macro_rules! construct {
    ($head:expr) => {
        Cons($head, Box::new(Nil))
    };
    ($head:expr,$($tail:tt)*) => {
        Cons($head, Box::new(construct!($($tail)*)))
    };
}
fn main() {
    let lst = construct!(3, 1, 5);
    println!("lst: {:?}", lst);
}

This will give us the following output:

lst: Cons(3, Cons(1, Cons(5, Nil)))

Let’s break it down. We have two match expressions. Either the macro will receive only one argument. In that case we can return a Cons(item, Box::new(Nil)) list, since we know that we will not receive any more items. The second pattern will match any argument, followed by zero or more TokenTrees and comma seperated. Then we take the first argument, wrap it in our Cons and construct it by recursively calling the macro on the rest of our arguments. It will expand like this:

let lst = construct!(3, 1, 5);
// => Cons(3, construct!(1, 5))
// => Cons(3, Box::new(Cons(1, construct!(5))))
// => Cons(3, Box::new(Cons(1, Box::new(Cons(5, Box::new(Nil))))))

Accidental Or Unavoidable?

The way macros work in rust are - especially compared to other programming languages - very nice to work with. The fact that rust even has macros was a surprise to me when I first heard of the language, since it is a low level language. Other low level languages, such as C or C++ do not support macros. But they are a very powerful tool and the concept of macros is not an accidental one.