🎓 A1Learner
🔍

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?