Rust DRY Macros
A DRY macro is a macro that writes repeated code from one pattern. DRY means Do Not Repeat Yourself.
The same shape, written twice
Two functions do the same job on two lists. Only the name and the operator differ:
fn add_all(xs: &mut Vec<i32>, ys: &Vec<i32>) {
for (x, y) in xs.iter_mut().zip(ys.iter()) { *x += *y; }
}
fn mul_all(xs: &mut Vec<i32>, ys: &Vec<i32>) {
for (x, y) in xs.iter_mut().zip(ys.iter()) { *x *= *y; }
}
fn main() {
let mut a = vec![1, 2, 3];
add_all(&mut a, &vec![10, 20, 30]);
println!("after add = {:?}", a);
let mut b = vec![2, 3, 4];
mul_all(&mut b, &vec![10, 10, 10]);
println!("after mul = {:?}", b);
}A macro writes the function
The caller chooses the new function's name — something no plain function or generic can do. A family of functions that differs only in names, traits or operators can now be written once:
macro_rules! make_getter {
($name:ident, $value:expr) => {
fn $name() -> i32 { $value }
};
}
make_getter!(answer, 42);
println!("answer = {}", answer());Capture the operator as a token
An operator like += is one token tree, so $op:tt captures it — ready to print in a message with stringify!, or to paste into generated code:
macro_rules! describe_op {
($op:tt) => { println!("operator: {}", stringify!($op)) };
}
describe_op!(+=);One macro calls another
In the closing example, op! calls assert_equal_len! first, so the length check is also written once. The same layering works anywhere:
macro_rules! check_small {
($v:expr) => { assert!($v < 100, "too big: {}", $v) };
}
macro_rules! store {
($v:expr) => {{ check_small!($v); $v }};
}
println!("stored = {}", store!(7));Generate a test per case
A test macro writes one #[test] fn $name for each case it is given. One pattern, as many tests as you list:
macro_rules! math_test {
($name:ident, $a:expr, $b:expr, $want:expr) => {
#[test]
fn $name() { assert_eq!($a + $b, $want); }
};
}
math_test!(adds_small, 1, 2, 3);Example
use std::ops::{Add, Mul};
macro_rules! assert_equal_len {
($a:expr, $b:expr, $func:ident, $op:tt) => {
assert!($a.len() == $b.len(), "{}: length mismatch for {}", stringify!($func), stringify!($op));
};
}
macro_rules! op {
($func:ident, $bound:ident, $op:tt, $method:ident) => {
fn $func<T: $bound<T, Output = T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
assert_equal_len!(xs, ys, $func, $op);
for (x, y) in xs.iter_mut().zip(ys.iter()) {
*x = $bound::$method(*x, *y);
}
}
};
}
op!(add_assign, Add, +=, add);
op!(mul_assign, Mul, *=, mul);
fn main() {
let mut x = vec![1, 2, 3];
add_assign(&mut x, &vec![10, 20, 30]);
println!("after add = {:?}", x);
let mut y = vec![2, 3, 4];
mul_assign(&mut y, &vec![10, 10, 10]);
println!("after mul = {:?}", y);
}Key points
- One fix in the macro repairs every generated function at once — copy-pasted functions must each be fixed by hand
- Generics vary the types a function accepts
- They do not vary its name or its operator
- That gap is what a generating macro fills
- DRY says each fact once.
- From one pattern, a macro writes many functions, and it can vary their names and operators.
In this example, op! is a function-writing macro. It captures a function name, a trait name, an operator token and a method name. The call op!(add_assign, Add, +=, add) writes a generic add_assign function. The next call writes mul_assign from the same pattern. Inside each written function, assert_equal_len! checks both lists share a length. It is a second macro, called by the first. It uses the $op token only to name the operator in its message. main adds 10, 20, 30 item by item. It prints after add = [11, 22, 33]. Then it multiplies and prints after mul = [20, 30, 40]. Two functions, one written pattern.
Note
DRY macros shine in test suites and operator families
If only one function is ever generated, write the function plainly instead
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Which designator captures an operator like += for the op! macro?