liveBook · Manning

Declarative macro

As explained in Write Powerful Rust Macros

macro_rules! is Rust's declaration syntax for defining declarative macros from matcher and transcriber rules.

A declarative macro is a Rust code generator defined with macro_rules!. It consists of matcher–transcriber rules: a matcher recognizes the tokens passed at an invocation, captures selected pieces, and a transcriber emits replacement code at that call site. The pattern-like syntax makes it a practical fit for repeated code shapes rather than arbitrary inspection or transformation of existing items. section 2.0 section 2.5

Define a macro with macro_rules! followed by its name and one or more delimited rules. Each rule has a matcher, =>, and a delimited transcriber; separate multiple rules with semicolons. Matchers and invocations may use parentheses, brackets, or braces. Invoke the result as name!(...), name![...], or name!{...}. A macro must normally be declared before use, and visibility matters because generated code is resolved where it expands. section 2.1.1 section 2.1.2 section 2.2.1

Listing 2.1 my_vec, our first declarative macro
macro_rules! my_vec {      #1
    () => [             #2
        Vec::new()
    ];                   #3
    (make an empty vec) => (      #4
        Vec::new()
    );        #5
    {$x:expr} => {
        {
            let mut v = Vec::new();
            v.push($x);
            v
        }
    };  #6
    [$($x:expr),+] => (
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )+
            v
        }
    )             
}

fn main() {
    let empty: Vec<i32> = my_vec![];
    println!("{:?}", empty);                #7
    let also_empty: Vec<i32> = my_vec!(make an empty vec);
    println!("{:?}", also_empty);          
    let three_numbers = my_vec!(1, 2, 3);
    println!("{:?}", three_numbers);         #8
}

The my_vec macro makes rule selection visible: empty input produces Vec::new, one expression pushes one value, and a repeated expression matcher builds a vector from many values. section 2.1

Matchers can offer several input forms and capture typed token fragments for reuse in generated code:

  • Use ident for a name, as in a macro that generates an impl block for a supplied newtype. section 2.2.3
  • Use ty when generated code must vary by a supplied type; a default rule can recursively invoke a more general rule. section 2.2.3
  • Use expr and repetition such as $($x:expr),+ to accept one or more expressions. section 2.1 section 2.2.5

Defaults without overloaded functions

A greeting macro can match either one literal or two. The one-argument rule expands to a normal base_greeting_fn call with "Hello"; the two-argument rule passes the caller’s greeting. This provides default arguments while leaving the underlying behavior in an ordinary function. section 2.2.1

Macros are especially useful for removing boilerplate, providing varargs or defaults, building small DSLs, and expressing recursive composition patterns that are awkward with ordinary Rust functions. A getter-generating macro can eliminate repeated impl methods across newtypes; a compose macro can recursively nest compose_two for any supplied sequence of functions. Macro recursion operates on syntax, however, so a seemingly terminating value-level condition does not stop recursive expansion. section 2.2 section 2.2.3 section 2.2.5

Expansion happens at compile time, and generated code receives normal compiler checks. Declarative macro hygiene prevents local identifiers from unintentionally colliding with external names. Prefer them as the simpler, minimal-setup option when matcher–transcriber rules suffice; move to procedural macros when the task requires lower-level token access or manipulating existing structs. section 1.3 section 3.0

How Write Powerful Rust Macros covers it

How deep it goes
Explains rule syntax, captures, repetition, recursion, visibility, hygiene, and failure cases, while deferring lower-level token manipulation to procedural macros.
Examples
Demonstrates my_vec, greeting defaults, generated newtype getters, recursive composition, currying closures, and generated AWS error conversions.
What it assumes
Assumes Rust familiarity beyond introductory macro syntax and introduces declarative macros as the simpler entry point before procedural macros.
What you can do after
Build it from scratch by defining matcher–transcriber rules, capturing fragments, repeating inputs, and recursively delegating default cases.
How it connects
Connects matcher–transcriber rules to pattern matching, macro hygiene, newtypes, function composition, currying, DSLs, and procedural macros.

Also explained in

Compare how 3 books explain declarative macro →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Write Powerful Rust Macros. The section links point to the parts of the book it draws on.