A macro generates code at compile time, before normal type checking happens — which is why vec![1, 2, 3] can accept any number of arguments of any single type, something an ordinary function's fixed signature could never do. The trailing ! is what marks a macro call and distinguishes it visually from a function call. macro_rules! defines a declarative macro by matching the call's syntax against a pattern and expanding to a code template.
Macros you already use every day
vec!, println!, format!, and assert_eq! are all macros, not functions — that's why vec! can take a variable number of comma-separated values and println! can accept a format string plus any number of arguments, neither of which an ordinary Rust function signature supports.
let v = vec![1, 2, 3, 4, 5]; // vec! — variable argument count
println!("{} and {}", "hello", 42); // println! — format string + args
let s = format!("{}-{}", "a", "b"); // format! — like println! but returns a StringWriting a declarative macro with macro_rules!
A macro_rules! definition matches against one or more patterns, each producing a code template. This example creates a HashMap literal, something Rust's standard library has no built-in syntax for.
macro_rules! hashmap {
($($key:expr => $val:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $val);)*
map
}};
}
let scores = hashmap!{
"Alice" => 90,
"Bob" => 85,
};Why a macro instead of a function
Macros solve problems ordinary functions structurally cannot: taking a variable number of arguments of different types, generating repetitive boilerplate code, or running entirely before type checking (which is how the derive macros behind #[derive(Debug)] work). The tradeoff is that macros are harder to read and debug than functions, so idiomatic Rust reaches for a function first and a macro only when a function genuinely cannot do the job.