412: Macro Repetition Patterns
Tutorial Video
Text description (accessibility)
This video demonstrates the "412: Macro Repetition Patterns" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Variadic functions (accepting any number of arguments) are fundamental to ergonomic APIs: `println!`, `vec!`, `format!`. Key difference from OCaml: 1. **Native variadic**: OCaml functions can accept lists/options making many macro use cases unnecessary; Rust requires macros for variadic behavior.
Tutorial
The Problem
Variadic functions (accepting any number of arguments) are fundamental to ergonomic APIs: println!, vec!, format!. In Rust, regular functions cannot be variadic — they have fixed arities. macro_rules! repetition syntax $($pattern),* matches zero or more occurrences and expands them, enabling macros that accept any number of arguments. This is how vec![1, 2, 3] works for any length, and how println!("{} {}", a, b) accepts different format argument counts.
Repetition patterns appear in every variadic macro: assert_matches!, matches!, dbg!, join! in async code, and custom test helpers.
🎯 Learning Outcomes
$(...),* (zero or more) and $(...),+ (one or more) repetition syntax$(,)? improves ergonomicsCode Example
macro_rules! sum {
() => { 0 };
($first:expr $(, $rest:expr)*) => {
$first $(+ $rest)*
};
}
macro_rules! product {
() => { 1 };
($first:expr $(, $rest:expr)*) => {
$first $(* $rest)*
};
}
fn main() {
println!("sum: {}", sum!(1, 2, 3, 4, 5));
println!("product: {}", product!(2, 3, 4));
}Key Differences
$tt); OCaml lists are homogeneous.OCaml Approach
OCaml functions are natively variadic through list arguments or optional parameters. List.fold_left (+) 0 values sums any list without macros. For syntax-level repetition (like match arm generation), OCaml uses PPX. The [%test_eq: int] x y syntax from Jane Street's ppx_jane generates comparison code through AST transformation — similar to Rust macro repetition but through a different mechanism.
Full Source
#![allow(clippy::all)]
//! Macro Repetition Patterns
//!
//! Using $(...),* and $(...),+ for variadic macros.
/// Sum any number of values.
#[macro_export]
macro_rules! sum {
() => { 0 };
($first:expr $(, $rest:expr)*) => {
$first $(+ $rest)*
};
}
/// Product of any number of values.
#[macro_export]
macro_rules! product {
() => { 1 };
($first:expr $(, $rest:expr)*) => {
$first $(* $rest)*
};
}
/// All values greater than threshold.
#[macro_export]
macro_rules! all_gt {
($threshold:expr; $($val:expr),+ $(,)?) => {
true $(&& ($val > $threshold))+
};
}
/// Any value equals target.
#[macro_export]
macro_rules! any_eq {
($target:expr; $($val:expr),+ $(,)?) => {
false $(|| ($val == $target))+
};
}
/// Create a HashMap from key-value pairs.
#[macro_export]
macro_rules! hashmap {
() => {
::std::collections::HashMap::new()
};
($($key:expr => $value:expr),+ $(,)?) => {
{
let mut map = ::std::collections::HashMap::new();
$(map.insert($key, $value);)+
map
}
};
}
/// Create a struct with fields from macro.
#[macro_export]
macro_rules! define_struct {
($name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
#[derive(Debug, Default, Clone, PartialEq)]
pub struct $name {
$(pub $field: $ty,)*
}
};
}
define_struct!(Config {
host: String,
port: u16,
timeout: u32,
});
/// Print a table of key-value pairs.
#[macro_export]
macro_rules! print_table {
($($key:expr => $val:expr),* $(,)?) => {
$(println!("{:>20}: {}", $key, $val);)*
};
}
/// Count the number of arguments.
#[macro_export]
macro_rules! count_args {
() => { 0usize };
($first:expr $(, $rest:expr)*) => {
1usize $(+ { let _ = $rest; 1usize })*
};
}
/// Create a Vec with a transformation applied.
#[macro_export]
macro_rules! vec_transform {
($f:expr; $($x:expr),* $(,)?) => {
vec![$($f($x)),*]
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_empty() {
assert_eq!(sum!(), 0);
}
#[test]
fn test_sum_single() {
assert_eq!(sum!(42), 42);
}
#[test]
fn test_sum_multiple() {
assert_eq!(sum!(1, 2, 3, 4, 5), 15);
}
#[test]
fn test_product_empty() {
assert_eq!(product!(), 1);
}
#[test]
fn test_product_multiple() {
assert_eq!(product!(2, 3, 4), 24);
}
#[test]
fn test_all_gt_true() {
assert!(all_gt!(0; 1, 2, 3, 4, 5));
}
#[test]
fn test_all_gt_false() {
assert!(!all_gt!(2; 1, 2, 3, 4, 5));
}
#[test]
fn test_any_eq_true() {
assert!(any_eq!(3; 1, 2, 3, 4, 5));
}
#[test]
fn test_any_eq_false() {
assert!(!any_eq!(10; 1, 2, 3, 4, 5));
}
#[test]
fn test_hashmap_empty() {
let m: std::collections::HashMap<&str, i32> = hashmap!();
assert!(m.is_empty());
}
#[test]
fn test_hashmap_entries() {
let m = hashmap! {
"a" => 1,
"b" => 2,
};
assert_eq!(m["a"], 1);
assert_eq!(m["b"], 2);
}
#[test]
fn test_define_struct() {
let cfg = Config {
host: "localhost".to_string(),
port: 8080,
timeout: 30,
};
assert_eq!(cfg.port, 8080);
}
#[test]
fn test_count_args() {
assert_eq!(count_args!(), 0);
assert_eq!(count_args!(1), 1);
assert_eq!(count_args!(1, 2, 3), 3);
assert_eq!(count_args!(1, 2, 3, 4, 5), 5);
}
#[test]
fn test_vec_transform() {
let v = vec_transform!(|x| x * 2; 1, 2, 3);
assert_eq!(v, vec![2, 4, 6]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_empty() {
assert_eq!(sum!(), 0);
}
#[test]
fn test_sum_single() {
assert_eq!(sum!(42), 42);
}
#[test]
fn test_sum_multiple() {
assert_eq!(sum!(1, 2, 3, 4, 5), 15);
}
#[test]
fn test_product_empty() {
assert_eq!(product!(), 1);
}
#[test]
fn test_product_multiple() {
assert_eq!(product!(2, 3, 4), 24);
}
#[test]
fn test_all_gt_true() {
assert!(all_gt!(0; 1, 2, 3, 4, 5));
}
#[test]
fn test_all_gt_false() {
assert!(!all_gt!(2; 1, 2, 3, 4, 5));
}
#[test]
fn test_any_eq_true() {
assert!(any_eq!(3; 1, 2, 3, 4, 5));
}
#[test]
fn test_any_eq_false() {
assert!(!any_eq!(10; 1, 2, 3, 4, 5));
}
#[test]
fn test_hashmap_empty() {
let m: std::collections::HashMap<&str, i32> = hashmap!();
assert!(m.is_empty());
}
#[test]
fn test_hashmap_entries() {
let m = hashmap! {
"a" => 1,
"b" => 2,
};
assert_eq!(m["a"], 1);
assert_eq!(m["b"], 2);
}
#[test]
fn test_define_struct() {
let cfg = Config {
host: "localhost".to_string(),
port: 8080,
timeout: 30,
};
assert_eq!(cfg.port, 8080);
}
#[test]
fn test_count_args() {
assert_eq!(count_args!(), 0);
assert_eq!(count_args!(1), 1);
assert_eq!(count_args!(1, 2, 3), 3);
assert_eq!(count_args!(1, 2, 3, 4, 5), 5);
}
#[test]
fn test_vec_transform() {
let v = vec_transform!(|x| x * 2; 1, 2, 3);
assert_eq!(v, vec![2, 4, 6]);
}
}
Deep Comparison
OCaml vs Rust: Macro Repetition
Side-by-Side Code
OCaml — Variadic via lists
let sum_list xs = List.fold_left (+) 0 xs
let product_list xs = List.fold_left ( * ) 1 xs
let () =
Printf.printf "sum: %d\n" (sum_list [1;2;3;4;5]);
Printf.printf "product: %d\n" (product_list [2;3;4])
Rust — Macro repetition
macro_rules! sum {
() => { 0 };
($first:expr $(, $rest:expr)*) => {
$first $(+ $rest)*
};
}
macro_rules! product {
() => { 1 };
($first:expr $(, $rest:expr)*) => {
$first $(* $rest)*
};
}
fn main() {
println!("sum: {}", sum!(1, 2, 3, 4, 5));
println!("product: {}", product!(2, 3, 4));
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Variadic args | Lists | Macro repetition |
| Syntax | [1;2;3] | sum!(1, 2, 3) |
| Type checking | At call site | During expansion |
| Performance | List allocation | Zero-cost (inlined) |
Repetition Patterns
// Zero or more (*)
$(pattern),*
// One or more (+)
$(pattern),+
// Zero or one (?)
$(pattern)?
Examples
// Zero or more items
macro_rules! vec_of {
($($x:expr),* $(,)?) => { vec![$($x),*] };
}
// At least one item
macro_rules! min {
($x:expr $(, $y:expr)+) => { ... };
}
// Optional trailing comma
macro_rules! map {
($($k:expr => $v:expr),+ $(,)?) => { ... };
}
5 Takeaways
[1;2;3] vs sum!(1, 2, 3).
No runtime overhead from list construction.
$(...)* = zero or more; $(...)+ = one or more.**Choose based on whether empty input is valid.
$(,)? makes it optional.**Common pattern for comma-separated macros.
$(+ $rest)* repeats the + for each captured $rest.
Exercises
max_of!(a, b, c, ...) analogously to min_of! — returning the maximum of any number of comparable expressions.cartesian!(($a1, $a2, ...), ($b1, $b2, ...)) that generates a Vec of all pairs. Use nested repetition.assert_all_eq!(($a, $b), ($c, $d), ...) that asserts each pair is equal, with the pair index in the panic message so failures identify which pair failed.