RustArena

Step 3 of 6

Repetition

Macros really shine when they accept any number of inputs. That's repetition: $( ... ) with a separator and a repetition operator.

macro_rules! my_vec {
    ( $( $x:expr ),* $(,)? ) => {{
        let mut v = Vec::new();
        $( v.push($x); )*
        v
    }};
}

macro_rules! print_all {
    ( $( $label:literal : $value:expr ),+ ) => {
        $( println!("{:>6}: {}", $label, $value); )+
    };
}

fn main() {
    let scores = my_vec![90, 72, 85,];
    println!("{:?}", scores);
    print_all!("best": 90, "worst": 72, "count": scores.len());
}
[90, 72, 85]
  best: 90
 worst: 72
 count: 3

How to read it:

Here's (simplified) how std's own vec! is written. $crate means "the crate this macro comes from", so the macro works even if the caller has never imported Vec:

macro_rules! vec {
    () => ( $crate::vec::Vec::new() );
    ($elem:expr; $n:expr) => ( $crate::vec::from_elem($elem, $n) );
    ($($x:expr),+ $(,)?) => ( <[_]>::into_vec($crate::boxed::Box::new([$($x),+])) );
}

In your own macros, write full paths like ::std::collections::HashMap for the same reason.

Common mistakes

Using a repeated variable outside a repetition:

macro_rules! pairs {
    ($($x:expr),*) => {
        vec![$x]
    };
}

fn main() {
    let v: Vec<i32> = pairs![1, 2];
    println!("{:?}", v);
}
error: variable `x` is still repeating at this depth

$x stands for a whole list, so it must appear inside a $( ... )* in the transcriber too: vec![$($x),*].

Forgetting the trailing comma rule: without $(,)?, the call my_vec![1, 2, 3,] fails with unexpected end of macro invocation, because after the last comma the matcher expects another expression.

An empty call with let mut: my_vec![] expands to a let mut v that's never changed, so the compiler warns variable does not need to be mutable. Give the empty case its own rule (() => { Vec::new() };), as std does. (Like vec![], an empty call still needs a type annotation from the caller, such as let v: Vec<i32> = my_vec![];.)

Your task

Write a hashmap! macro: hashmap!{ "a" => 1, "b" => 2 } builds a HashMap containing those entries. Allow an optional trailing comma. The starter already has the rule for hashmap!{} (an empty map); add a rule for one or more key => value pairs. Use the full path ::std::collections::HashMap so callers don't need a use.

Challenge 2

Write sum!, which adds any number of values: sum!(1, 2, 3) expands to 1 + 2 + 3. sum!() is 0, and a trailing comma is allowed. It must also work for floats (sum!(1.5, 2.5) is 4.0), so don't start the expansion with an integer 0 when there are values: match the first value separately and repeat + $rest for the others.

Challenge 3

Write enum_str!, which generates an enum plus two helper methods, a common way to avoid writing the same list of variants three times. enum_str!(Level { Debug, Info, Warn, Error }) should expand to:

stringify!(tokens) turns tokens into a string literal at compile time: stringify!(Warn) is "Warn". Allow a trailing comma after the last variant, and use the macro to create Level as shown.

Previous: Fragment specifiers and generating code Next: Recursive macros