How macros make your Rust life easier and generally better
How macros make your life easier
Hello fellow Rustaceans!
Let’s explore a topic that has been quite foreign to me for a long time: macros. After reading this post you’ll be able to automate all kinds of tasks. (Or skip the reading and go to GitHub directly)
Like everybody, I dread copy-paste programming of boiler plate code. However I also never looked into macros, partly because the C/C++ macro engine was still in the back of my mind, operating as a fancy search-and-replace step before compilation; partly because Rust’s macros looked complex and I didn’t want to deal with that… . But I was wrong. Totally wrong in fact - macros make your code more efficient, less repetitive, and in general better without much complexity at all.
What Rust macros are
Contrary to the C preprocessor, Rust’s macros are not simple text replacements - but part of the normal compilation process. This means they behave more like functions, inserted into the code before it is compiled to binary - not as text but directly into the AST (or in other words it’s programming programming - or metaprogramming). In this realm there is already some type-safety, making unexpected behavior rare. The effects can be seen when you tinker a little bit with the upcoming examples - the macro will compile just fine, but the compiler is going to complain when using invalid types.
Additionally, there are two types of macros: procedural and declarative. Procedural macros are more complex to write but (IMHO) easier to use - we have all used #[derive(Debug)]
to make the Debug
trait implementation magically appear. Yet I want to leave those for another time and explore declarative macros here (those with the !
in their names). Let’s stat off simple: how to declare a declarative macro?
Declaring declarative macros
As appropriate, a macro is declared using a macro. macro_rules!
is the birthplace for every macro. Let’s create a very simple macro that returns the result of a simple calculation: 1 + 1
.
macro_rules! two {
() => { 1 + 1 }
}
assert_eq!(2, two!());
As any macro, this has three parts:
- a name (
two
) - an (empty) matcher (
()
) - a transcriber containing an expression (
{ 1 + 1 }
)
The name of the “matcher” gives away its purpose: it matches the invocation and replaces it with the transcriber. The matching is done exactly (as we’ll see further down) and a macro’s name can have several matchers. This gives you the power to create domain specific languages and extend Rust’s syntax. Let’s update the macro above:
macro_rules! calc {
(two) => { 1 + 1 };
(three) => { 1 + 2 }
}
assert_eq!(2, calc!(two));
assert_eq!(3, calc!(three));
Note that two
or three
are just … words that match a pattern in the macro declaration. Any combination of characters can do the same.
What about parameters?
So now that we know about declaring macros and what they do, how about we add some complexity with parameters? Here is an example:
macro_rules! repeat_n_times {
($n:expr, $text:expr) => {(0..$n).map(|_| format!("{}", $text)).collect::<Vec<String>>()}
}
assert_eq!(vec!["Hello", "Hello", "Hello"], repeat_n_times!(3, "Hello"));
Each parameter has a name ($name
) and a type (called designator) that matches on the parameters that are passed into the macro. There are several designators to match to (incl. reference):
- ident Identifier
- block Block
- stmt Statement
- expr Expression
- pat Pattern
- ty Type
- lifetime Lifetime
- literal Literals
- path Path
- meta Meta items
While I certainly have not touched some of these (yet?), the most important ones are expressions, blocks, statements, and identifiers. Let’s look at another example that creates an entire callable function with a macro.
#[derive(PartialEq, Debug)]
struct Response(usize);
// Declare a function in a macro!
macro_rules! handler {
($i: ident, $body: block) => {
fn $i () -> Response $body
}
}
handler!(status_handler, { Response(200)});
assert_eq!(Response(200), status_handler());
If you like that, check out quick-error which generate custom trait implementations using only one large macro. This not only simplifies error handling, it also shows that it’s possible to create entire types including implementations.
Repetitions, repetitions, repetitions
Earlier, we saw a macro repeating a statement using a regular for
loop. However, this requires us to 1) pass in a number and 2) results in a for
loop being inserted instead of a direct expansion. As you have expected, Rust supports expanding parameter lists into multiple statements directly as well. For that, let’s look at the following macro:
use std::collections::BTreeSet;
macro_rules! set {
( $( $item:expr ),* ) => {
{
let mut s = BTreeSet::new();
$(s.insert($item);)*
s
}
};
}
let actual = set!("a", "b", "c", "a");
// basically also what the macro does:
let mut desired = BTreeSet::new();
desired.insert("a");
desired.insert("b");
desired.insert("c");
assert_eq!(actual, desired);
Somewhat similar to regular expressions, the *
allows an expression contained in the parenthesis to be repeated zero or more times. $()
designates the part that should be repeated, ,
is the seperator between those repetitions (Note: The only special characters allowed in macro patterns are =>
, ,
, and ;
). Inside the macro’s body we use this repeated variable - again - within a $()*
scope to run code as often as there are items in the variable.
So, combining all of these aspects leads to very useful macros. One could be to instantiate a map Ruby style:
macro_rules! key_value {
( $cls: ty, $( $key:expr => $value:expr ),* ) => {
{
let mut s = <$cls>::new();
$(s.insert($key, $value);)*
s
}
};
}
let actual = key_value!(BTreeMap<&str, usize>, "hello" => 2, "world" => 1);
let mut desired = BTreeMap::new();
desired.insert("hello", 2);
desired.insert("world", 1);
assert_eq!(actual, desired);
// or a different kind of map:
let actual = key_value!(HashMap<&str, usize>, "hello" => 2, "world" => 1);
let mut desired = HashMap::new();
desired.insert("hello", 2);
desired.insert("world", 1);
assert_eq!(actual, desired);
Import-Export business
Having all these macros living freely in the files can become messy quickly and they should be grouped together - the same way that functions/structs/… are kept in modules to make them a lot more managable. Macros are not exported by default, so the attribute #[macro_export]
on top of the macro takes care of selectively exporting the macro to outside the module, similar to what pub
does. Here is the same example from above, with the macro in a submodule:
mod helpers {
#[macro_export] // try removing this to see the error
macro_rules! key_value {
( $cls: ty, $( $key:expr => $value:expr ),* ) => {
{
let mut s = <$cls>::new();
$(
s.insert($key, $value);
)*
s
}
};
}
}
let actual = key_value!(BTreeMap<&str, usize>, "hello" => 2, "world" => 1);
let mut desired = BTreeMap::new();
desired.insert("hello", 2);
desired.insert("world", 1);
assert_eq!(actual, desired);
// or a different kind of map:
let actual = key_value!(HashMap<&str, usize>, "hello" => 2, "world" => 1);
let mut desired = HashMap::new();
desired.insert("hello", 2);
desired.insert("world", 1);
assert_eq!(actual, desired);
And that’s it! Creating macros is done, however when using macros from an unknown source you can also declare a #[macro_use]
attribute over an extern crate mycrate
(yes, also in the Rust 2018 Edition) to stay safe and only import a subset.
Go, be lazy
This is it - how to safe time and effort with Rust macros. Since there is no learning like tinkering and playing with the thing you want to be good at, so check out the GitHub repository for this post. For any additional inspiration, here are some things that you can (and should?) use macros for:
- Unwrapping read access to interor mutabilty objects
- Creating a DSL
- Auto-implementing a trait
However you end up using them, macros make a great addition to your Rust programming skills which gives you more time to do the things you love: making the borrow checker happy.
Never miss any of my posts and follow me on Twitter, or even better, add the RSS feed to your reader!