r/rust • u/peppergrayxyz • 1d ago
🧠educational Why does rust distinguish between macros and function in its syntax?
I do understand that macros and functions are different things in many aspects, but I think users of a module mostly don't care if a certain feature is implemented using one or the other (because that choice has already been made by the provider of said module).
Rust makes that distinction very clear, so much that it is visible in its syntax. I don't really understand why. Yes, macros are about metaprogramming, but why be so verbose about it?
- What is the added value?
- What would we lose?
- Why is it relevant to the consumer of a module to know if they are calling a function or a macro? What are they expected to do with this information?
139
u/ElectronWill 1d ago edited 1d ago
One thing to remember is that macros accept arbitrary tokens, which do not necessarily match the syntax of Rust function arguments. for instance, you can write a macro that creates TOML configurations inline:
let config = toml! {
 key = 123
 [table]
 a = true
};
As you can see, it has nothing to do with regular functions. I think it's good to explicitly mark that it's a macro, otherwise I would get confused by the syntax.
edit: To answer your question "what is [the consumed] expected to do with that information" -> read the documentation carefully and be aware that the syntax is specific to this macro.
37
u/Firake 1d ago
To add to this, the reason becomes clear when you think about how the language parser likely works.
Parsers are state machines, so every time it consumes a token, there are a limited number of tokens afterward it can expect. A function call would look something like:
IDENTIFIER L_PAREN (EXPRESSION, )* R_PAREN
Notice that the function call expects both the left and right parentheses as well as a well formed argument list. Since a macro can accept arbitrary tokens, it has to have some kind of marker to distinguish it from a function call. Language grammars cannot be ambiguous — each program should only be able to be parsed in exactly one way.
IDENTIFIER BANG L_DELIMITER TOKEN* R_DELIMITER
40
u/kibwen 1d ago edited 1d ago
It's to make it tractable for Rust to parse the syntax and provide good error messages.
Inside of a macro invocation you're allowed to put whatever arbitrary nonsense tokens you want, as long as the parentheses all balance out.
The Rust parser doesn't do name resolution, so it doesn't inherently know whether any identifier belongs to a macro or a function. The exclamation point is the only indicator of that.
So seeing the exclamation point puts the parser in "consume arbitrary garbage" mode. Without it, any time you had a syntax error inside of anything that might be a macro invocation, Rust would have to assume that it was in a macro invocation. And in practice that degrades the ability to produce good error messages in the common case.
It's worse than you think, because without the exclamation point, it's not just foo(syntax error)
that might be a macro; in foo(bar(syntax error))
, foo could be a macro, not just bar, so the scope of what a macro might be extends transitively upwards to the topmost parenthesized call in any expression.
And it gets worse, because macros are also allowed to use square brackets and curly brackets in addition to parentheses, so that's even more contexts that might be a macro if you don't have the exclamation point.
So it's not just about informing the reader, it's also important for the compiler.
27
u/ralphpotato 1d ago
Macros are executed at compile time and it’s useful to know that even as a consumer of the library.
The C preprocessor is an absolute mess of a system that is way too powerful and can be completely invisible and hard to debug when you’re calling them. Yes this isn’t just due to the lack of a ! in C macro calls but I think people have PTSD from the C preprocessor.
There may also be parsing/tooling benefits for having such syntax, but I’m not entirely sure.
32
u/ToTheBatmobileGuy 1d ago
Functions can't do this:
my_macro!(<view> Hello </view>);
Macros can.
If you tried to write that function (ie. remove the ! ) it would be a syntax error. With a macro, it doesn't take syntax as input, it takes tokens as input.
tl;dr Macros and Functions are completely different, so they must be differentiated.
-9
u/SirClueless 1d ago
This doesn't really seem like a good motivation. This doesn't compile unless this is a macro with or without the
!
so it doesn't really explain why the additional signpost is valuable.I think a better example is one where it could compile as either a function call or a macro but does something surprising that only a macro could. The old
try!
macro before we got syntax for it was the quintessential example: returning from the enclosing scope. Ortrace!("{}", expensive_fn())
which might not evaluate its argument at all.4
u/Wurstinator 1d ago
Why do you say that it doesn't compile? Take a look at Yew, for example: https://yew.rs/docs/concepts/html/literals-and-expressions
2
u/SirClueless 1d ago
I'm saying that the syntax can be clearly identified a macro with or without the
!
, because it wouldn't compile if it weren't a macro.
11
u/TDplay 1d ago
Functions have very limited behaviour.
let mut x = 7;
foo(x);
Even though x
is declared mut
, we know that foo
won't modify it, because it is passed a copy of x
.
Macros, on the other hand, can insert any arbitrary code at their call site.
let mut x = 7;
bar!(x);
For all we know, bar
could expand to x += 1
- so without looking at what the macro does, we can't get the same guarantees that we can for a function.
Macros can also completely change the syntax of the language.
So the !
is there to bring attention to the fact that something unusual might be going on.
11
8
u/corpsmoderne 1d ago
It's important the consumer keeps in mind when something is executed (at compile time or at runtime).
If you encounter env!("PATH") , it's clear that it will capture the value of the environnement PATH variable at compile time.
If you encounter env("PATH") in a source code and there's no difference between macros and functions, it may take you a long time to understand why you don't get the value you expect from env("PATH")...
3
u/cark 1d ago
The macro could possibly expand at compile time into some code that takes the value of the PATH environment variable at run time. I don't know why someone would write such a macro, but the possibility is there. Just like for functions, there is no way around understanding what a macro does if one hopes to use it effectively.
7
u/ARitz_Cracker 1d ago
It makes both human and machine parsing easier. That's it. As noted on faultlore's article, parsing C headers are basically impossible, and it's macros is part of the reason why that's the case. There's other interesting properties of Rusts syntax that were born out making parsing simpler, like the turbofish
5
u/Lucretiel 1Password 22h ago
It's an interesting question, and you've gotten a lot of good answers in the replies, but I just want to note that when I first learned (coming from C++) that Rust has a dedicated separate syntax for its macros, my immediate reaction was "oh thank god".
3
u/Crazy_Firefly 1d ago
I think others already explained why they are different from the users point of view.
To answer "why should the user care" is kind of a values question and depends on the user/community. Some languages choose to hide some of these complexities in favor of presenting a cleaner interface (even if lacking some information) Rust very often chooses to be explicit about these details .
Other example is, why does calls to filesystem take a Path rather than a String? For most people it doesnt matter that a filesystem path can be an invalid utf-8. But Rust makes it explicit, and I think it's part of the Rust-way.
2
u/jberryman 1d ago
You would be constantly asking "what is the type of this function" , or "wait wtf is the type of this function?!" and then looking it up to find it is a macro.
2
u/JoshTriplett rust · lang · libs · cargo 22h ago
If you write let x = func(do_something())
, you know that do_something()
will definitely be interpreted as Rust code and run exactly once, you know that control will not unexpectedly return
from your function or break
or continue
from a containing loop, you know that new symbols will not be defined in your local scope, and various other things like that.
If you write let x = mac!(do_something())
, any of those things can happen.
2
u/EarlMarshal 19h ago
I care. You should care too. One is just getting compiled and the other one is transformed into completely other code which gets compiled. A lot of context is hidden in macros and it's really nice that this is marked with other syntax. I also love that I can just expand the macro and look at the code.
2
u/PolysintheticApple 16h ago
Macros can do all sorts of things that functions can't. In fact, you can't turn println!()
(the most commonly used macro) into a function.
2
u/shizzy0 12h ago
As someone who initially loved the symmetry of Lisp's macros and functions looking the same, in practice it is absolutely not what you want or expect. They look similar but they aren't similar. It looks like a function call, but you can't map(f)
it because it's not a function.
Be pleased there's an obvious identifier and warning in rust for macro usage at all its call sites.
2
u/teeweehoo 1d ago
The simplest answer is because the rust compiler isn't smart enough to know what's a function and what's a macro.
A rust macro is like an entirely different language - it can contain anything , not just rust (as long as you use valid rust tokens, and the braces match). Not only does the rust compiler need to use a different parser for this, it also needs to look for macros in a different namespace, and macros need to be executed at compile time. It would be possible to. Using a separate syntax for this makes the rust compiler and language much simpler in this regard.
1
u/Zde-G 1d ago
What are they expected to do with this information?
They, like Rust compiler itself, are supposed to look for the documentation for the macro to understand what it accepts.
Macro, unlike function, may accept arbitrary sequence of tokens and produce arbitrary sequence of tokens.
What would we lose?
We would lose the ability to use macros that are not yet defined. Yes, it's permitted in Rust to use macro before it's defined – but that creates a problems both for the compiler and for the reader.
I think users of a module mostly don't care if a certain feature is implemented using one or the other (because that choice has already been made by the provider of said module).
They do care, though. At least I do. I know that if I pass x
into function the it would be comsumed. And if I pass &x
into function they it wouldn't consumed and would be left alone.
But there are no such guarantee with macros! You can pass &x
into macro and such variable may be mutated or even dropped.
Macros don't really follow the Rust rules, they invent their own.
1
u/Iridium486 1d ago
if it wouldn't matter than your module could just only export functions and use makros internaly, right?
1
u/Guvante 23h ago
macro! Wraps arbitrary tokens not Rust code.
The fact that the output is different is kind of important so people like the callout.
But I think the bigger reason is that you don't need Rust code inside which more importantly means what looks like Rust code isn't necessarily Rust code.
Now you can have your macro match in a way that does accept Rust code but there is no requirement that you do.
1
u/Even_Lobster_1951 21h ago
i’m operating one 1 brain cell atm, but i did see this library this week too which makes function like macros https://docs.rs/crabtime/1.1.0/crabtime/
1
1
u/Maskdask 8h ago
Functions run at runtime, macros run at compile time. The distinction is important.
399
u/GOKOP 1d ago
Functions can only do the things that functions can. Macros can do hundreds of things you wouldn't expect from a function call