The Case For Macros

25 October 2018

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.