What are macros capable of that functions can't do?

It appears to me that macros are capable of:

  • Executing code at compile time
  • Accepting variables arguments
  • Generating code from input expressions

From a language design point of view, it seems to me like variable arguments could be achieved by passing in an array of arguments or simply be supported by functions. Rust chooses to only allow macros do to this though.. is there a reason?

Anything else?

Trying to figure it out online and everything I'm finding seems to dive in too quickly into the details. Above all, I would like to better understand from a high level what are the problems that are impossible to solve without macros.

Thanks!

Yes, var-args-like functionality can be achieved using… if it’s values of the same types arrays, otherwise also possibly tuples (using e.g. traits implemented up to a certain reasonable maximal size… or for all tuples using not-yet-existing compiler support for abstracting over all tuples?).

But var-args is rarely the main reason why a macro is used. Executing code, in particular code that can validate extra stuff, at compiler time, or generating code, are the main applications in my experience. E.g. the println! macro can interpret the format string at compile-time and make sure that its placeholder match with the number of values provided… Code generation for the sake of saving the need to write lots of code is most clearly demonstrated in the form of derive macros.

Sometimes, macros can be useful to offer safe abstractions around certain concepts that are hard or impossible to offer as an API using only functions. Stack pinning comes to mind, or self-referencing structs. Or pin projections.

Sometimes, macros are just used because people want to create their own better-looking (or more convenient) syntax. E.g. the vec! macro is probably unnecessary, as far as I’m aware, but it gives us nice and concise vec![x, y, z,] and vec![foo; 42] syntaxes.

I haven’t worked with it, but I’ve heard about crates that could go as far as validating SQL queries (not only syntactically, but even against your database schema) at compile time.

4 Likes
  • Executing code at compile time

You can also execute code at compile time with build scripts and const fns (with different restrictions and available input information).

  • Accepting variables arguments

Most of the use cases of merely variable numbers of arguments can be solved using array or tuple literals: foo([a, b, c]). When a macro accepts variable numbers of items, it's generally not just generating a loop over them; it's generating code specifically for each item, which can be statically checked and more efficient (or less efficient).

  • Generating code from input expressions

This is what macros are for: situations where the thing that you want to do cannot be done cleanly without lots of boilerplate code that would be difficult to maintain or mistake-prone, and the macro lets you reduce the amount of code you have to write, in a way a plain function cannot do. Or, sometimes, can't be done without unsafe code (e.g. pin_mut! for futures) — the macro can ensure that only correct unsafe code is generated.

1 Like

Note that one such place is supported and widely used in Rust already. That means it's already possible in nightly Rust.

Most macros that I have used are used as a poor-man's-replacement for C++ templates. Every time when you try to look on source of some function and see something which doesn't even mention name of that function… that's it. C++ template implemented via macro in Rust.

Good example. C++ does that with templates, of course.

Requires compile-time reflection, but can be done with templates, too.

SQLx does that, yes.

Rust's macros are quite versatile but they are limited by the fact that they are executing before types, variables and other such things exist.

That's why some things which C++ templates can do easily macros can do only with great difficulty while some other things are very hard to do with templates, but easy with macros.

This can be written as Vec::from([x, y, z]) since Rust 1.44.

I think that if it had been possible from the beginning then the vec macro would not have existed, but technical limitations made it necessary to have such a macro.

I'm curious what it is about macros that makes this easier?

So it's not that it has a safe way of checking if unsafe code is generated, just that it's reusable in different places and has therefore been vetted, making it safer? I'm not seeing how it's safer than a function which is similar.

Thanks for the great replies so far everyone!

Typing. Functions deals with typing, macros are untyped. That's why JavaScript doesn't need macros: in a language where you can look on function and find out what's inside macros are just a regular functions.

Of course dynamic typing have other drawbacks but it obviates the need for macros.

It's a bit strange to compare solution which may exist to another which couldn't thus it's hard to answer that question.

Note that using Vec::from is much more limiting when it comes to repeated values- arrays can only repeat Copy values a constant number of times, whereas vec! can handle a dynamic number of Clone values.

It's sorta like how by verifying the appropriate invariants, you can have safe functions that wrap unsafe ones. The only difference is that functions and macros have an entirely different set of invariants that they can enforce.

1 Like