ExamplesBy LevelBy TopicLearning Paths
411 Fundamental

411: `macro_rules!` Basics

Functional Programming

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

  • • Understand how macro_rules! pattern-matches on token trees to generate code
  • • Learn the basic fragment designators: expr, ident, ty, block, tt
  • • See how multiple arms enable function-like overloading on syntax
  • • Understand hygiene: macro-generated variables don't leak into the call site scope
  • • Learn when macros are appropriate vs. functions (variadic args, syntax capture, codegen)
  • Code 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

  • Built-in vs. external: Rust's macro_rules! is in the language; OCaml requires PPX dependencies and dune plugin configuration.
  • Hygiene: Rust macros are hygienic — generated identifiers don't conflict with caller-scope names; OCaml PPX extensions have no hygienic scope.
  • Pattern matching: Rust matches on token streams with named fragments; OCaml PPX transforms the AST directly using OCaml pattern matching on Parsetree types.
  • Error messages: Rust macro expansion errors show the expansion point; OCaml PPX errors appear at the expansion site but can be harder to trace.
  • 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Compile-time codegenppx (external)Built-in macro_rules!
    Variadic functionsLists or labeled argsMacro repetition $(...)*
    File/line info__LOC__file!(), line!()
    HygieneN/AHygienic by default
    Pattern matchingIn functionsIn macro arms

    macro_rules! Syntax

    macro_rules! name {
        // Pattern 1
        ($pattern:designator) => {
            expansion
        };
        // Pattern 2
        ($x:expr, $($rest:expr),*) => {
            expansion with repetition
        };
    }
    

    Designators

  • expr — expression
  • ident — identifier
  • ty — type
  • pat — pattern
  • tt — token tree (anything)
  • block — code block

  • Repetition

    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 one

  • 5 Takeaways

  • OCaml lacks built-in macros; ppx is external.
  • Rust's macro_rules! is part of the language.

  • Macros enable variadic "functions" at compile time.
  • min_of!(1, 2, 3, 4) works for any number of args.

  • Pattern matching in macros is powerful.
  • Different expansions based on input shape.

  • **file!() and line!() enable better error messages.**
  • Available inside macros for debugging.

  • Macros are hygienic — variables don't leak.
  • A variable inside a macro won't shadow outer scope.

    Exercises

  • Hash map macro: Implement hashmap!{ "key" => value, ... } that creates a HashMap from key-value pairs. Handle the empty case hashmap!{} returning an empty map.
  • Assert with context: Create 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.
  • Timing macro: Implement time_it!(label, expr) that evaluates expr, prints "{label} took {elapsed:?}" using std::time::Instant, and returns the expression value.
  • Open Source Repos