Skip to Content
Course content

190: Recursive Macros for Variadic-Like Behavior

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've already used the repetition patterns—those $(...)* blocks—to handle multiple arguments in macros. For 90% of cases, that's all you need. But there's a specific wall you'll hit when you need to perform an operation that depends on the result of the previous element, or when you need to "consume" arguments one by one to build a complex nested structure.

When you hit that wall, you need recursion. But recursive macros are a bit of a minefield if you aren't careful with your patterns.

The Infinite Expansion Loop

I once saw a junior dev try to write a macro that would print a list of values along with their position in the list. They knew they couldn't easily track an index inside a standard repetition block, so they tried recursion. Here is a simplified version of the code they wrote:

macro_rules! print_list {
    ($head:expr, $($tail:expr),*) => {
        println!("Processing: {}", $head);
        print_list!($($tail),*);
    };
}

fn main() {
    print_list!(10, 20, 30);
}

If you try to compile this, the compiler is going to scream at you. You'll likely see an error about "recursion limit reached." The problem is that the macro expands print_list!(10, 20, 30) into println!(...) and a call to print_list!(20, 30). That expands into println!(...) and print_list!(30). That expands into println!(...) and print_list!().

But look at the pattern: ($head:expr, $($tail:expr),*). This pattern requires at least one expression (the $head). When the macro finally reaches the empty call print_list!(), it no longer matches that pattern. In many languages, this would just be a runtime error, but in Rust's declarative macros, if no pattern matches, it's a compilation error. More importantly, if you had written the pattern slightly differently, you could accidentally create a loop that never consumes the tokens, causing the compiler to hang until it hits its internal recursion limit.

Establishing the Base Case

To fix this, we have to treat the macro like a recursive function. You need a "base case"—a pattern that matches when there are no more elements to process and simply stops the recursion.

The trick is to define the base case before or after the recursive case, depending on how you want to handle empty calls. Usually, we put the most specific patterns first. Let's add a case for when only one element is left, and a case for when the macro is called with nothing.

macro_rules! print_list {
    // Base case 1: No arguments left
    () => {};

    // Base case 2: Exactly one argument left
    ($head:expr) => {
        println!("Processing: {}", $head);
    };

    // Recursive case: One head and at least one tail element
    ($head:expr, $($tail:expr),*) => {
        println!("Processing: {}", $head);
        print_list!($($tail),*);
    };
}

Now, the expansion for print_list!(10, 20, 30) looks like this:

  • print_list!(10, 20, 30) matches the recursive case $\rightarrow$ prints 10, calls print_list!(20, 30)
  • print_list!(20, 30) matches the recursive case $\rightarrow$ prints 20, calls print_list!(30)
  • print_list!(30) matches the single-argument base case $\rightarrow$ prints 30, and stops.

Handling State Through Recursion

Now, you might be wondering: "Why not just use a for loop inside the macro?" You can't, because macros operate on tokens, not values. But recursion allows you to pass "state" forward. If we wanted to keep track of the index, we can't use a local variable, but we can pass an extra argument through the recursive calls.

I find this pattern incredibly useful when building DSLs or complex configuration macros. Here is how you'd actually implement that index tracking:

macro_rules! print_indexed {
    // The public entry point
    (@step $idx:expr, $head:expr, $($tail:expr),*) => {
        println!("Item {}: {}", $idx, $head);
        print_indexed!(@step $idx + 1, $($tail),*);
    };

    // Base case for the internal step
    (@step $idx:expr, $head:expr) => {
        println!("Item {}: {}", $idx, $head);
    };

    // The actual macro the user calls
    ($($elems:expr),*) => {
        print_indexed!(@step 0, $($elems),*);
    };
}

fn main() {
    print_indexed!("Apple", "Banana", "Cherry");
}

Notice the @step prefix. This is a common idiom in advanced Rust macros called "internal rules." By prefixing the recursive pattern with @, I'm signaling that this isn't a pattern the user should call directly; it's a helper for the macro's internal logic. It prevents the user from accidentally triggering the internal state logic.




📋 Practical Task

Exercise: The Recursive Product Accumulator

Your task is to create a macro called sum_product! that calculates the product of a variadic list of numbers using recursion. However, there's a twist: to practice the "internal rule" pattern, the macro must use a hidden helper rule to accumulate the total.

Requirements:

  • The macro should be callable as sum_product!(a, b, c, ...).
  • It must use an internal rule (e.g., starting with @acc) to pass the current product through the recursive calls.
  • The initial accumulator value should be 1.
  • The macro must handle:
    • Zero arguments (should return 0).
    • A single argument (should return that argument).
    • Multiple arguments.

Example Usage:

let result = sum_product!(2, 3, 4); 
assert_eq!(result, 24);

let empty = sum_product!(); 
assert_eq!(empty, 0);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.