414: Recursive Macro Patterns
Tutorial Video
Text description (accessibility)
This video demonstrates the "414: Recursive Macro Patterns" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Some computations and code generation tasks have naturally recursive structure: counting elements, reversing lists, checking if a value equals any of N options. Key difference from OCaml: 1. **Compile vs. runtime**: Rust macro recursion is compile
Tutorial
The Problem
Some computations and code generation tasks have naturally recursive structure: counting elements, reversing lists, checking if a value equals any of N options. In functions, recursion is straightforward. In macros, recursion works by having one arm handle the base case (empty input) and another arm peel off one element and recursively invoke the macro on the remainder. This compile-time recursion is bounded by Rust's macro_recursion_limit (default 128) and computes entirely at compile time, producing efficient code with no runtime recursion.
Recursive macros are used in matches!, compile-time string concatenation, recursive DSL parsers, and any macro needing to process a variable-length input sequentially.
🎯 Learning Outcomes
@acc (accumulator) pattern for building up results recursivelycount! uses 1 + count!(rest) to count at compile timereverse_list!(@acc [...] ...) pattern for tail-recursive macrosrecursion_limit attribute and when to increase itCode Example
macro_rules! count {
() => { 0 };
($head:expr $(, $tail:expr)*) => {
1 + count!($($tail),*)
};
}
macro_rules! reverse_list {
(@acc [$($acc:expr),*]) => { [$($acc),*] };
(@acc [$($acc:expr),*] $head:expr $(, $tail:expr)*) => {
reverse_list!(@acc [$head $(, $acc)*] $($tail),*)
};
($($x:expr),*) => { reverse_list!(@acc [] $($x),*) };
}
fn main() {
println!("count: {}", count!(1, 2, 3, 4, 5));
let rev = reverse_list![1, 2, 3];
}Key Differences
recursion_limit (default 128, adjustable with #![recursion_limit = "512"]); OCaml has no equivalent constraint.@acc arm naming convention is a Rust macro idiom for internal state; OCaml uses standard accumulating function parameters.OCaml Approach
OCaml PPX generates recursive code via recursive OCaml functions operating on AST nodes. A compile-time list-reversal PPX processes the actual list data during compilation. OCaml's let[@unrolled] rec attribute can unroll recursive functions. For true compile-time computation, OCaml uses its module system with recursive functors, though these are rarer than Rust's macro recursion.
Full Source
#![allow(clippy::all)]
//! Recursive Macro Patterns
//!
//! Macros that call themselves for compile-time computation.
/// Count elements at compile time.
#[macro_export]
macro_rules! count {
() => { 0usize };
($head:expr $(, $tail:expr)*) => {
1usize + count!($($tail),*)
};
}
/// Reverse an array at compile time using accumulator pattern.
#[macro_export]
macro_rules! reverse_list {
// Internal: accumulator-based recursion
(@acc [$($acc:expr),*]) => {
[$($acc),*]
};
(@acc [$($acc:expr),*] $head:expr $(, $tail:expr)*) => {
reverse_list!(@acc [$head $(, $acc)*] $($tail),*)
};
// Public entry point
($($x:expr),* $(,)?) => {
reverse_list!(@acc [] $($x),*)
};
}
/// Check if value equals any of the options.
#[macro_export]
macro_rules! one_of {
($val:expr, $single:expr) => {
$val == $single
};
($val:expr, $first:expr $(, $rest:expr)+) => {
$val == $first || one_of!($val $(, $rest)+)
};
}
/// Concatenate strings with separator.
#[macro_export]
macro_rules! join_with {
($sep:expr; $single:expr) => {
$single.to_string()
};
($sep:expr; $first:expr $(, $rest:expr)+) => {
format!("{}{}{}", $first, $sep, join_with!($sep; $($rest),+))
};
}
/// Sum values recursively.
#[macro_export]
macro_rules! sum_rec {
() => { 0 };
($single:expr) => { $single };
($first:expr $(, $rest:expr)+) => {
$first + sum_rec!($($rest),+)
};
}
/// Maximum of values recursively.
#[macro_export]
macro_rules! max_rec {
($single:expr) => { $single };
($a:expr, $b:expr) => {
if $a > $b { $a } else { $b }
};
($first:expr $(, $rest:expr)+) => {
{
let rest_max = max_rec!($($rest),+);
if $first > rest_max { $first } else { rest_max }
}
};
}
/// Generate nested tuples (cons-list style).
#[macro_export]
macro_rules! nested_tuple {
($single:expr) => { $single };
($head:expr, $($tail:expr),+) => {
($head, nested_tuple!($($tail),+))
};
}
/// Power of 2 at compile time (for small n).
#[macro_export]
macro_rules! pow2 {
(0) => {
1usize
};
(1) => {
2usize
};
(2) => {
4usize
};
(3) => {
8usize
};
(4) => {
16usize
};
(5) => {
32usize
};
(6) => {
64usize
};
(7) => {
128usize
};
(8) => {
256usize
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_count_empty() {
assert_eq!(count!(), 0);
}
#[test]
fn test_count_single() {
assert_eq!(count!(42), 1);
}
#[test]
fn test_count_multiple() {
assert_eq!(count!(1, 2, 3, 4, 5), 5);
}
#[test]
fn test_reverse_list() {
let rev = reverse_list![1, 2, 3, 4, 5];
assert_eq!(rev, [5, 4, 3, 2, 1]);
}
#[test]
fn test_reverse_list_empty() {
let rev: [i32; 0] = reverse_list![];
assert_eq!(rev, []);
}
#[test]
fn test_one_of_true() {
assert!(one_of!(3, 1, 2, 3, 4, 5));
}
#[test]
fn test_one_of_false() {
assert!(!one_of!(10, 1, 2, 3, 4, 5));
}
#[test]
fn test_one_of_single() {
assert!(one_of!(42, 42));
assert!(!one_of!(42, 0));
}
#[test]
fn test_join_with() {
assert_eq!(join_with!(", "; "a", "b", "c"), "a, b, c");
assert_eq!(join_with!("-"; "x"), "x");
}
#[test]
fn test_sum_rec() {
assert_eq!(sum_rec!(), 0);
assert_eq!(sum_rec!(5), 5);
assert_eq!(sum_rec!(1, 2, 3, 4), 10);
}
#[test]
fn test_max_rec() {
assert_eq!(max_rec!(42), 42);
assert_eq!(max_rec!(3, 7), 7);
assert_eq!(max_rec!(5, 2, 9, 1, 6), 9);
}
#[test]
fn test_nested_tuple() {
let t = nested_tuple!(1, 2, 3);
assert_eq!(t, (1, (2, 3)));
}
#[test]
fn test_pow2() {
assert_eq!(pow2!(0), 1);
assert_eq!(pow2!(3), 8);
assert_eq!(pow2!(8), 256);
}
}#[cfg(test)]
mod tests {
#[test]
fn test_count_empty() {
assert_eq!(count!(), 0);
}
#[test]
fn test_count_single() {
assert_eq!(count!(42), 1);
}
#[test]
fn test_count_multiple() {
assert_eq!(count!(1, 2, 3, 4, 5), 5);
}
#[test]
fn test_reverse_list() {
let rev = reverse_list![1, 2, 3, 4, 5];
assert_eq!(rev, [5, 4, 3, 2, 1]);
}
#[test]
fn test_reverse_list_empty() {
let rev: [i32; 0] = reverse_list![];
assert_eq!(rev, []);
}
#[test]
fn test_one_of_true() {
assert!(one_of!(3, 1, 2, 3, 4, 5));
}
#[test]
fn test_one_of_false() {
assert!(!one_of!(10, 1, 2, 3, 4, 5));
}
#[test]
fn test_one_of_single() {
assert!(one_of!(42, 42));
assert!(!one_of!(42, 0));
}
#[test]
fn test_join_with() {
assert_eq!(join_with!(", "; "a", "b", "c"), "a, b, c");
assert_eq!(join_with!("-"; "x"), "x");
}
#[test]
fn test_sum_rec() {
assert_eq!(sum_rec!(), 0);
assert_eq!(sum_rec!(5), 5);
assert_eq!(sum_rec!(1, 2, 3, 4), 10);
}
#[test]
fn test_max_rec() {
assert_eq!(max_rec!(42), 42);
assert_eq!(max_rec!(3, 7), 7);
assert_eq!(max_rec!(5, 2, 9, 1, 6), 9);
}
#[test]
fn test_nested_tuple() {
let t = nested_tuple!(1, 2, 3);
assert_eq!(t, (1, (2, 3)));
}
#[test]
fn test_pow2() {
assert_eq!(pow2!(0), 1);
assert_eq!(pow2!(3), 8);
assert_eq!(pow2!(8), 256);
}
}
Deep Comparison
OCaml vs Rust: Recursive Macros
Side-by-Side Code
OCaml — Recursive functions
let rec count = function
| [] -> 0
| _ :: t -> 1 + count t
let rec reverse_acc acc = function
| [] -> acc
| h :: t -> reverse_acc (h :: acc) t
let reverse lst = reverse_acc [] lst
let () =
Printf.printf "count: %d\n" (count [1;2;3;4;5]);
Printf.printf "reverse: %s\n"
(String.concat "," (List.map string_of_int (reverse [1;2;3])))
Rust — Recursive macros
macro_rules! count {
() => { 0 };
($head:expr $(, $tail:expr)*) => {
1 + count!($($tail),*)
};
}
macro_rules! reverse_list {
(@acc [$($acc:expr),*]) => { [$($acc),*] };
(@acc [$($acc:expr),*] $head:expr $(, $tail:expr)*) => {
reverse_list!(@acc [$head $(, $acc)*] $($tail),*)
};
($($x:expr),*) => { reverse_list!(@acc [] $($x),*) };
}
fn main() {
println!("count: {}", count!(1, 2, 3, 4, 5));
let rev = reverse_list![1, 2, 3];
}
Comparison Table
| Aspect | OCaml Functions | Rust Macros |
|---|---|---|
| When runs | Runtime | Compile time |
| Recursion limit | Stack (TCO helps) | Expansion limit (~128) |
| Accumulator | Function parameter | Internal @acc pattern |
| Type checking | Static, once | Per expansion site |
The Accumulator Pattern
For compile-time list reversal:
macro_rules! reverse_list {
// Internal rules (start with @)
(@acc [$($acc:expr),*]) => {
[$($acc),*] // Base: return accumulated
};
(@acc [$($acc:expr),*] $head:expr $(, $tail:expr)*) => {
// Move head to front of accumulator, recurse with tail
reverse_list!(@acc [$head $(, $acc)*] $($tail),*)
};
// Public entry point
($($x:expr),*) => {
reverse_list!(@acc [] $($x),*) // Start with empty acc
};
}
The @acc prefix marks internal rules that users shouldn't call directly.
5 Takeaways
The result is fully expanded before execution.
@internal patterns for helper rules.**Keeps the public API clean.
Same technique as tail-recursive functions.
Very deep macro recursion will fail to compile.
Errors show up at call sites, not macro definition.
Exercises
const_sum!(1, 2, 3, 4, 5) that expands to the literal sum 1 + 2 + 3 + 4 + 5 and verify with const SUM: i32 = const_sum!(1, 2, 3).unique_types!(i32, f64, i32, String, f64) using recursive macro expansion to deduplicate types and generate a tuple type containing only unique types from the list.zip!(($a1, $a2, ...), ($b1, $b2, ...)) using recursive macro arms that peel one element from each list per step and accumulate ($a1, $b1) pairs.