Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
190: Recursive Macros for Variadic-Like Behavior
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, callsprint_list!(20, 30)print_list!(20, 30)matches the recursive case $\rightarrow$ prints 20, callsprint_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);There are no comments for now.