The Case For Macros
I know a few Rustaceans who are wary of macros. One privately admitted to hating them with a passion. They are right; macros can make code harder to understand (both for humans and computers, for example many clippy lints have an explicit check to only lint outside of macros), so they should be used with some caution.
On the other hand, Rust’s type system, while powerful, sometimes won’t allow deduplicating a piece of code, because it’s not always possible to make the types line up correctly. In those cases, we are given a choice:
- Copy & Paste the code
- Generate the code
- Use a macro
All of those options have their problems and benefits.
Copying the code means there are N varsions of it around. So the code is now much harder to navigate, because you have the same shapes N times. What was one bug before now turns into N bugs, and if the code is large, it’s easy to get lost and miss one of them. This also applies to other changes, which you must follow through to all N versions. One benefit is that it’s easy to manage if some of the N versions actually need to diverge. Also the compiler won’t need to expand anything, so in theory this could lead to a faster build than the other options (however, this is usually negligible, unless the macros become very complex).
Code generation ensures consistency between the N versions of the code. It is
the most flexible approach, because while the end product must be valid code,
the parts from which it is generated need not be. Plus, you can use whatever
libraries you have to aid your code generation task. However, in Rust this
means you have a build.rs
that must be compiled and run before your build can
even start, so it imposes a cost in turnaround time even for a plain
cargo check
. Also because the generation code lives outside the crate code,
it is easier to overlook. I personally find code generation too powerful and
keep its use reserved for cases where not even macros work (for an example,
mutagen uses code generation for both the library and plugin).
Finally. macros (and I mostly mean macro-by-example here) strike a balance
between power and descriptiveness. Every Rustacean uses them to some extent
(for example println!
for writing out stuff). Macros have often been used to
prototype language features (case in point: The ?
operator began its life as
a try!
macro). The standard library uses macros extensively to define
operations on integer types, even down to the doc comments. This allows us to
generate code and docs with working doctests from a single source, and given
the amount of code it’s blazingly fast.
That said, macros do have their downsides: They can obfuscate the code, can be hard to debug if something goes wrong. Though rustc affords us some tools to alleviate this, notably macro tracing and expanded output, those tools are not yet very refined. Thus one best keeps the complexity within macros as low as possible, for now.
Even if the tools leave something to be desired for now doesn’t mean it has to be like this: For example, railroad is a cool idea to improve the documentation of macros on the input side. It doesn’t tell us what the results will be, though. The space appears somewhat under-explored. How about a crate to “animate” macro expansion with << and >> keys? Do LISPers have other cool tools to deal with macro difficulties (I know that at least in clisp the REPL has ways to show the expansion of a macro)?