411: `macro_rules!` Basics
Tutorial Video
Text description (accessibility)
This video demonstrates the "411: `macro_rules!` Basics" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Some code patterns cannot be abstracted with functions. Key difference from OCaml: 1. **Built
Tutorial
The Problem
Some code patterns cannot be abstracted with functions. A function that takes a variable number of arguments of different types, generates different code branches depending on the caller's syntax, or captures the source location of its invocation requires metaprogramming. Rust's macro_rules! is the declarative macro system: pattern-match on the input token stream, transform it, and emit generated code. Unlike C preprocessor macros, macro_rules! is hygienic (generated identifiers don't leak), syntactically aware, and integrated into the module system.
macro_rules! powers vec!, println!, assert_eq!, format!, todo!, unimplemented!, and hundreds of third-party macros.
🎯 Learning Outcomes
macro_rules! pattern-matches on token trees to generate codeexpr, ident, ty, block, ttCode Example
macro_rules! min_of {
($a:expr) => { $a };
($a:expr, $($rest:expr),+) => {{
let rest = min_of!($($rest),+);
if $a < rest { $a } else { rest }
}};
}
macro_rules! repeat {
($n:expr, $body:block) => {
for _ in 0..$n $body
};
}
fn main() {
repeat!(3, { println!("Hello"); });
println!("min={}", min_of!(3, 7, 1, 9));
}Key Differences
macro_rules! is in the language; OCaml requires PPX dependencies and dune plugin configuration.Parsetree types.OCaml Approach
OCaml uses PPX (preprocessor extensions) and camlp5 for syntactic metaprogramming. PPX attributes ([@attr]) and extensions ([%ext ...]) trigger compile-time code transformation via external plugins. ppx_deriving generates trait implementations; ppx_sexp_conv generates S-expression serialization. Unlike Rust's built-in macro_rules!, OCaml's PPX requires external libraries and a build system plugin.
Full Source
#![allow(clippy::all)]
//! macro_rules! Basics
//!
//! Declarative macros for code generation at compile time.
/// Simple assertion macro with custom failure message.
#[macro_export]
macro_rules! check_eq {
($left:expr, $right:expr) => {
if $left != $right {
panic!(
"check_eq failed: {:?} != {:?} at {}:{}",
$left,
$right,
file!(),
line!()
);
}
};
($left:expr, $right:expr, $msg:expr) => {
if $left != $right {
panic!(
"check_eq failed ({}): {:?} != {:?} at {}:{}",
$msg,
$left,
$right,
file!(),
line!()
);
}
};
}
/// Repeat a block n times.
#[macro_export]
macro_rules! repeat {
($n:expr, $body:block) => {
for _ in 0..$n $body
};
}
/// Minimum of multiple values.
#[macro_export]
macro_rules! min_of {
($a:expr) => { $a };
($a:expr, $($rest:expr),+) => {
{
let first = $a;
let rest_min = min_of!($($rest),+);
if first < rest_min { first } else { rest_min }
}
};
}
/// Maximum of multiple values.
#[macro_export]
macro_rules! max_of {
($a:expr) => { $a };
($a:expr, $($rest:expr),+) => {
{
let first = $a;
let rest_max = max_of!($($rest),+);
if first > rest_max { first } else { rest_max }
}
};
}
/// HashMap literal macro.
#[macro_export]
macro_rules! map {
() => {
::std::collections::HashMap::new()
};
($($k:expr => $v:expr),+ $(,)?) => {
{
let mut m = ::std::collections::HashMap::new();
$(m.insert($k, $v);)+
m
}
};
}
/// Vec literal with transformation.
#[macro_export]
macro_rules! vec_map {
($transform:expr; $($item:expr),* $(,)?) => {
{
let f = $transform;
vec![$(f($item)),*]
}
};
}
/// Simple timing macro for benchmarking.
#[macro_export]
macro_rules! time_it {
($label:expr, $body:expr) => {{
let start = ::std::time::Instant::now();
let result = $body;
let elapsed = start.elapsed();
println!("{}: {:?}", $label, elapsed);
result
}};
}
/// Match guard with default.
#[macro_export]
macro_rules! with_default {
($opt:expr, $default:expr) => {
match $opt {
Some(v) => v,
None => $default,
}
};
}
// Public functions to demonstrate macro usage
/// Demonstrates min_of macro.
pub fn find_minimum(values: &[i32]) -> i32 {
match values {
[] => panic!("empty slice"),
[a] => *a,
[a, b] => min_of!(*a, *b),
[a, b, c] => min_of!(*a, *b, *c),
_ => *values.iter().min().unwrap(),
}
}
/// Demonstrates max_of macro.
pub fn find_maximum(values: &[i32]) -> i32 {
match values {
[] => panic!("empty slice"),
[a] => *a,
[a, b] => max_of!(*a, *b),
[a, b, c] => max_of!(*a, *b, *c),
_ => *values.iter().max().unwrap(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_eq_pass() {
check_eq!(2 + 2, 4);
check_eq!("hello".len(), 5, "string length");
}
#[test]
#[should_panic(expected = "check_eq failed")]
fn test_check_eq_fail() {
check_eq!(1, 2);
}
#[test]
fn test_min_of_single() {
assert_eq!(min_of!(42), 42);
}
#[test]
fn test_min_of_multiple() {
assert_eq!(min_of!(5, 3, 7, 1, 9), 1);
assert_eq!(min_of!(10, 20), 10);
assert_eq!(min_of!(1, 2, 3), 1);
}
#[test]
fn test_max_of_single() {
assert_eq!(max_of!(42), 42);
}
#[test]
fn test_max_of_multiple() {
assert_eq!(max_of!(5, 3, 7, 1, 9), 9);
assert_eq!(max_of!(10, 20), 20);
assert_eq!(max_of!(1, 2, 3), 3);
}
#[test]
fn test_map_macro_empty() {
let m: std::collections::HashMap<&str, i32> = map!();
assert!(m.is_empty());
}
#[test]
fn test_map_macro_entries() {
let m = map! {
"one" => 1,
"two" => 2,
"three" => 3,
};
assert_eq!(m["one"], 1);
assert_eq!(m["two"], 2);
assert_eq!(m["three"], 3);
}
#[test]
fn test_vec_map() {
let v = vec_map!(|x| x * 2; 1, 2, 3);
assert_eq!(v, vec![2, 4, 6]);
}
#[test]
fn test_with_default() {
let some_val: Option<i32> = Some(42);
let none_val: Option<i32> = None;
assert_eq!(with_default!(some_val, 0), 42);
assert_eq!(with_default!(none_val, 0), 0);
}
#[test]
fn test_repeat() {
let mut count = 0;
repeat!(5, {
count += 1;
});
assert_eq!(count, 5);
}
#[test]
fn test_find_minimum() {
assert_eq!(find_minimum(&[5, 3, 8]), 3);
assert_eq!(find_minimum(&[10]), 10);
}
#[test]
fn test_find_maximum() {
assert_eq!(find_maximum(&[5, 3, 8]), 8);
assert_eq!(find_maximum(&[10]), 10);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_eq_pass() {
check_eq!(2 + 2, 4);
check_eq!("hello".len(), 5, "string length");
}
#[test]
#[should_panic(expected = "check_eq failed")]
fn test_check_eq_fail() {
check_eq!(1, 2);
}
#[test]
fn test_min_of_single() {
assert_eq!(min_of!(42), 42);
}
#[test]
fn test_min_of_multiple() {
assert_eq!(min_of!(5, 3, 7, 1, 9), 1);
assert_eq!(min_of!(10, 20), 10);
assert_eq!(min_of!(1, 2, 3), 1);
}
#[test]
fn test_max_of_single() {
assert_eq!(max_of!(42), 42);
}
#[test]
fn test_max_of_multiple() {
assert_eq!(max_of!(5, 3, 7, 1, 9), 9);
assert_eq!(max_of!(10, 20), 20);
assert_eq!(max_of!(1, 2, 3), 3);
}
#[test]
fn test_map_macro_empty() {
let m: std::collections::HashMap<&str, i32> = map!();
assert!(m.is_empty());
}
#[test]
fn test_map_macro_entries() {
let m = map! {
"one" => 1,
"two" => 2,
"three" => 3,
};
assert_eq!(m["one"], 1);
assert_eq!(m["two"], 2);
assert_eq!(m["three"], 3);
}
#[test]
fn test_vec_map() {
let v = vec_map!(|x| x * 2; 1, 2, 3);
assert_eq!(v, vec![2, 4, 6]);
}
#[test]
fn test_with_default() {
let some_val: Option<i32> = Some(42);
let none_val: Option<i32> = None;
assert_eq!(with_default!(some_val, 0), 42);
assert_eq!(with_default!(none_val, 0), 0);
}
#[test]
fn test_repeat() {
let mut count = 0;
repeat!(5, {
count += 1;
});
assert_eq!(count, 5);
}
#[test]
fn test_find_minimum() {
assert_eq!(find_minimum(&[5, 3, 8]), 3);
assert_eq!(find_minimum(&[10]), 10);
}
#[test]
fn test_find_maximum() {
assert_eq!(find_maximum(&[5, 3, 8]), 8);
assert_eq!(find_maximum(&[10]), 10);
}
}
Deep Comparison
OCaml vs Rust: Declarative Macros
Side-by-Side Code
OCaml — Functions (no macros)
(* No macro_rules! equivalent in OCaml *)
(* Use functions or ppx preprocessors *)
let min_of a b = if a < b then a else b
let max_of a b = if a > b then a else b
let repeat n f =
for _ = 1 to n do f () done
let () =
repeat 3 (fun () -> print_endline "Hello");
Printf.printf "min=%d\n" (min_of 3 7)
Rust — macro_rules!
macro_rules! min_of {
($a:expr) => { $a };
($a:expr, $($rest:expr),+) => {{
let rest = min_of!($($rest),+);
if $a < rest { $a } else { rest }
}};
}
macro_rules! repeat {
($n:expr, $body:block) => {
for _ in 0..$n $body
};
}
fn main() {
repeat!(3, { println!("Hello"); });
println!("min={}", min_of!(3, 7, 1, 9));
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Compile-time codegen | ppx (external) | Built-in macro_rules! |
| Variadic functions | Lists or labeled args | Macro repetition $(...)* |
| File/line info | __LOC__ | file!(), line!() |
| Hygiene | N/A | Hygienic by default |
| Pattern matching | In functions | In macro arms |
macro_rules! Syntax
macro_rules! name {
// Pattern 1
($pattern:designator) => {
expansion
};
// Pattern 2
($x:expr, $($rest:expr),*) => {
expansion with repetition
};
}
Designators
expr — expressionident — identifierty — typepat — patterntt — token tree (anything)block — code blockRepetition
macro_rules! vec_of {
($($x:expr),* $(,)?) => {
vec![$($x),*]
};
}
let v = vec_of![1, 2, 3]; // vec![1, 2, 3]
$(...)* — zero or more$(...)+ — one or more$(...)? — zero or one5 Takeaways
Rust's macro_rules! is part of the language.
min_of!(1, 2, 3, 4) works for any number of args.
Different expansions based on input shape.
file!() and line!() enable better error messages.**Available inside macros for debugging.
A variable inside a macro won't shadow outer scope.
Exercises
hashmap!{ "key" => value, ... } that creates a HashMap from key-value pairs. Handle the empty case hashmap!{} returning an empty map.assert_between!(val, lo, hi) that panics with a message showing the value and the expected range, using file!() and line!() in the panic message.time_it!(label, expr) that evaluates expr, prints "{label} took {elapsed:?}" using std::time::Instant, and returns the expression value.