I found some spare time this past week and sat down with a nice brew and Jon Gjengset's excellent Crust of Rust video on declarative macros. For the longest time, macros have felt 'that last part of Rust that I haven't gotten around to checking out'. I've had a vague notion of what they are, but have never quite gotten to exploring them. However, Gjengset's video served as a perfect introduction to declarative macros, and was just enough to get me started.
One thing that was mentioned in the video that I have never thought about before, is that one of the simplest things to do with macros is simple substitution. In fact, that's all a declarative macro can do: given some input, it'll expand to a block of code. That suddenly gave me an idea for writing my own first macro.
Post purpose
This post is intended to be a very brief and basic introduction to declarative macros based on what have I found in the past week. For more comprehensive material, see the 'Further Reading' section at the end.
The post describes one very simple use case for macros, and that's all it's intended to do. In particular, this post will not disccuss
- macro syntax
- There will be no talk of macro syntax, of capture kinds and patterns, or of clever ways to count.
- other use cases
- This is not an exploration of all the ways in which you can use declarative macros or where they shine. This is a description of one case that solved a problem that's been irking me.
- proc macros
- Proc macros are a different subject, and is something I don't know much (or anything, really) about. They're both a form of metaprogramming, but from what I understand, proc macros are quite a bit more complex than declarative macros (and thus more powerful), so it's best left for later.
I assume a basic level of familiarity with Rust, but a deep understanding is not required.
My Little Macro 🦄
I was very excited when advanced slice patterns were stabilized in Rust 1.42. Among the things I'd been looking forward to was the ability to match on strings as if they were a slice of characters, similar to what you might do in Haskell or Elm. It wasn't immediately obvious how to do it, but I figured something out in the end[1]:
fn f(s: &str) {
match &s.chars().collect::<Vec<char>>() as &[char] {
['💘', .., '🦄'] => println!("<3 and horse"),
['💘', snd, .., '😪'] => println!("Love, {}, and sleeps", snd),
['💘', ..] => println!("Just <3"),
_ => {}
}
}
However, it's not immediately obvious what's happening on line 2: what exactly does &s.chars().collect::<Vec<char>>() as &[char]
mean? Sure, I can tell you that it turns the string into a char
slice for matching, but as it stands, it's quite the mouthful. Let's write a macro to make this cleaner and clearer!
Replacement as a form of abstraction
Because a declarative macro is nothing but text substitution[2], we should be able to simply abstract away the pesky line from above. Instead, we want to write something like this:
fn f(s: &str) {
match chars!(s) {
['💘', .., '🦄'] => println!("<3 and horse"),
['💘', snd, .., '😪'] => println!("Love, {}, and sleeps", snd),
['💘', ..] => println!("Just <3"),
_ => {}
}
}
To do this, we write a very simple macro with a single pattern:
macro_rules! chars {
($s:expr) => {
&$s.chars().collect::<Vec<char>>() as &[char]
};
}
All it does is replace the macro call (chars!(s)
) with the long incantation (&s.chars().collect::<Vec<char>>() as &[char]
) when compiling. It's incredibly simple, but it's also incredibly powerful and it's made the code look less cluttered and read better at the same time.
Further reading
If you want to know more about macros, here are some good resources to continue your journey:
- Jon Gjengset's Crust of Rust: Declarative Macros
- Gjengset spends roughly 90 minutes explaining and demonstrating macros in a very clear fashion by creating a macro that has the same functionality as the standard library's
vec!
macro. If you're looking for an introduction to what declarative macros are, I absolutely recommend you watch this. - The Little Book of Rust Macros
- Gjengset mentions this as a resource in his Crust of Rust video. It's a thorough intro to macros and includes everything from syntax to patterns to macro building blocks and a guide on how to implement esoteric languages using only macros.
- The Book, chapter 19.06
- As always, the Book is a valuable resource on all things Rust. Compared to the previous items on the list, this is much shorter, but it provides a strong high-level overview.
- The Rust Reference on macros
- Probably the densest resource on this list, the Rust Reference provides a short, yet comprehensive reference on macros. If you need to quickly look something up, this is a good bet.
Footnotes
Iterating over a string and collecting it into a vector of characters is probably not the most efficient way to work with strings, but it's the best way I've found to turn a string into a slice of (UTF-8) characters that can be matched on. If you've got a better solution for this, let me know! I've been looking for a while.
Well, saying it's just text substitution might be misleading: Because the expanded macro is parsed into the program's abstract syntax tree, it has to be valid Rust. See the first chapter of the Little Book of Rust Macros for more information.
- [1]
Iterating over a string and collecting it into a vector of characters is probably not the most efficient way to work with strings, but it's the best way I've found to turn a string into a slice of (UTF-8) characters that can be matched on. If you've got a better solution for this, let me know! I've been looking for a while.
- [2]
Well, saying it's just text substitution might be misleading: Because the expanded macro is parsed into the program's abstract syntax tree, it has to be valid Rust. See the first chapter of the Little Book of Rust Macros for more information.