Are macros unreadable just for me?

Something I'm running into right now.

Say: axum_login - Rust

There they jam a login_required! macro in the middle of the code. Not entirely sure what Backend is but, it's possible to figure that out with a bunch of clicking around.

But this breaks my code entirely. Here's what my editor shows.

This is not Rust code.

This is the "documentation" of the macro: login_required in axum_login - Rust

How am I supposed to figure out the types of what goes in and what comes out of this?

Also given how macros are only briefly touched upon in any Rust book, it's a bit discouraging to see them strewn around anywhere and everywhere.

4 Likes

Macros have important use cases, but it’s not clear why this one needs to exist. Over-use and under-documentation of macros in a library API should be taken as a reason to avoid that library.

17 Likes

Filing issues about bad docs on macros also seem worthwhile to me. Many macros in the Rust ecosystem are underdocumented.

12 Likes

I mean at that point I might as well stop using half the libraries on crates.io.

Just like teaching macros is an afterthought, maybe writing them is as well often?

3 Likes

I think that part of the problem is Rust relying on declarative macros to compensate gaps in the language. The standard library contains many examples: how many times have I been lost when I was trying to find the definition of a method, only to find several layers of macros used to define methods on several types? Typically methods on integer types.

It's also quite messy to read and understand. It's fine if it's used to generate a simple object, but when it becomes part of the code logic, it makes for painful understanding and code maintenance.

In this case, it looks like it's been (ab)used because Rust doesn't provide us with optional method parameters, though the doc isn't very loquacious.

18 Likes

These are not even a gaps, unfortunately. Rust have decreed that all the “gnarly” things that are handled with templates in C++ and comptime manipulations in Zig shouldn't exist… but world declined to comply and macros can be used to solve that dilemma.

The fact that they make code hard to read is unfortunate but there's cargo expand that may help in such cases.

Macros are what macros have always been in any language (or even applications):

A great tool to solve repetition but also an evil weapon for total obfuscation when misused.

Somehow reminds me on Lisp or even functional languages like Haskell: You can write higher functions and juggling with mathematical constructions that makes a program total incomprehensible.

2 Likes

The only thing we can do about it (when tempted to write a macro) is to try to find another solution involving traits, and be willing to tolerate code duplication when a macro is the only way to avoid it.

I'm guessing this is really not going to change, but I'd say having an obtuse meta-language core to a lot of functionality is really one of the worst things about Rust.

7 Likes

Maybe, but at some point you should stop obsessing with ability to easily read code and start thinking about ability to write it.

Rust without macros would have been so extremely hard to write that it probably wouldn't have gotten much adoption at all.

2 Likes

I'd say it's a lesser evil in some cases, when it's not possible to do otherwise. I'd rather use println! and vec! than the equivalent Rust code (which may still include built-in macros). It usually provides a more readable code where the macro is used, at the expense of readability of the macro itself, so at the expense of intelligibility of how it works.

The syntax has been designed to be distinct from Rust's, I think, so that it wouldn't be too confusing. The result is still hard to read unless you're familiar with it, especially in some complicated cases. At least, it's less problematic than C's preprocessor, which is a nasty hack and can produce even nastier side-effects.

On the other hand, declarative macros are a great tool when you need to instantiate objects in your unit tests or even in an API, like vec!. It's also generally handy in tests, where you must often repeat similar things. There, it doesn't bother the crate users since it's not public.

The more insidious problems I see with it, beside the lack of readability in std, are:

  • When it replaces a gap in the language, it might become a reason to postpone the original issue indefinitely. At this point, the macro has already been adopted, anyway. Note that it's sometimes per design because changing the language would make it less safe, or it would be too complicated to change, or the result would be less performant (e.g. complexity moving from compile time to runtime).
  • It might be OK when the macro is in the standard library, like format! or vec!, but when it's not (no hashmap!, hashset!, btreemap!, btreeset!, ...), everyone starts using their custom macros or one of the crates providing them [1]. They often look the same, but they don't all behave the same way, so that's a tiny risk. Or the crates are not maintained, any more.

Then, as you said, editors and IDEs struggle with it because they have to expand the code and analyze it. It's not rhetorical: in a series of unit tests, I finally had to replace many small declarative macros that were instantiating the objects under test because of the significant impact it had on the IDE performances.


  1. For example, hashmap_macro, map-macro, hmap, static_map_macros, maplit, velcro, and cute, most of which are still unstable (version 0.x). ↩︎

1 Like

Are there any languages where macros are easy tor read?

3 Likes

No, you're not alone. Couple years ago I also tried to figure out what's happening with my args in axum's macros, and it made no sense at all. Some more attentive reading of the docs gave implicit hints to what I had to do. Axum's magic is heavy.

On the other hand, when I checked how heavy the output code is, compared to other Web frameworks, Axum was the best one. (My simple tests)

1 Like

Complexity have to live somewhere. If you refuse to have complexity in macros then it's often pushed into the code and then compiler may or may not optimize it away.

2 Likes

Crystal is pretty good.

It always seemed to me that code is pretty complicated stuff. Now you want macros, a way to write code, which is complicated, in code, which is complicated. Seems to me the resulting mess cannot help but be complexity squared.

Also given how macros are only briefly touched upon in any Rust book, it's a bit discouraging to see them strewn around anywhere and everywhere.

I'm saying this for a long time. There is a rampant macro abuse in the Rust ecosystem.

3 Likes

Well, what do you expect? Macros are the simplest way to do practical metaprogramming. Generics and traits are nice but they expect completely orthogonal taxonomy of everything… our world is too messy for that. Macros give you practical way of handling these things.

Think about what would you need to implement serde without macros, Java-style… a lot of things that are either rejected on purpose or simply not implemented yet.

2 Likes

Regarding the book / tutorial part, Writing Powerful Rust Macros (by Sam Van Overmeire) is a very good book, but it's more focused on procedural macros. There's a good tutorial for declarative macros too, though it doesn't show more hairy cases or how to work around the recursive limitations, so I'd recommend it mostly if you're also interested in procedural macros.

There are a few website tutorials on declarative macros, like The Little Book of Rust Macros (which looks very interesting!) and Macros in Rust: A tutorial with examples. There's a section in the reference, too.

1 Like

I'm not aware of any other than the Lisps.