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:
- In the matcher,
$( $x:expr ),*means "zero or more expressions, separated by commas". The separator (here,) goes between the)and the operator. - The operators:
*is zero or more,+is one or more,?is zero or one. $(,)?at the end allows an optional trailing comma, somy_vec![90, 72, 85,]works. Every collection macro should allow one; people reorder lines and leave commas behind.- In the transcriber,
$( v.push($x); )*repeats its contents once for each$xthat was matched. Three inputs, threepushlines. - A repetition can capture several metavariables at once, like
$labeland$value. They're repeated together, pair by pair.
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:
enum Level { Debug, Info, Warn, Error }derivingDebug, Clone, Copy, PartialEqfn as_str(&self) -> &'static strreturning the variant's name (Level::Warn.as_str()is"Warn")fn all() -> Vec<Level>listing every variant in order
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