Why are bracket types so loosely checked in macro definitions and invocations?

There are three places where a bracket type can be chosen:

  • in the macro definition, around the parameters
  • in the macro definition, around the body
  • in the macro invocation, around the parameters.

Right now, the type of bracket used in each place matters next to nothing. The following code is completely valid:

macro_rules! my_macro {
    {} => [
        println!("test");
    ]
}

my_macro!();
my_macro!{};
my_macro![];

Notice the peculiar bracket types I used here: curly braces around parameters, square brackets around the code block, and all types of brackets in the invocation.

There's no restriction to which bracket types I can use. In the macro definition, instead of {} => [ I could have written [] => (, () => {, [] => [, {} => (, etc.

I stumbled upon this language property today and I wonder what the benefit of such loose bracket type checking is. To me, this seems inconsistent with the rest of the language:

  • In Rust, code block statements are always surrounded with curly braces (reference). Macro definitions are the only exception

  • In Rust, functions/collection literals/control constructs always use paranthesis, square brackets and curly braces, respectively.

    • It's my_function(); not my_function[] or my_function{}.
    • It's let arr = [1, 2, 3], not let arr = {1, 2, 3} or let arr = (1, 2, 3).
    • It's loop {}, not loop () or loop [].

    However, in macros, you can use whatever.

    • vec![1, 2, 3] is just as valid as vec!(1, 2, 3) and vec!{1, 2, 3}.
    • lazy_static! { } is just as valid as lazy_static! [ ] and lazy_static! ( )

    It seems like, again, macros are the only exception to Rust's usual consistency.

I wonder what the reasoning behind these decisions are. Is there a benefit to vec!{1, 2, 3} instead of vec![1, 2, 3]? Is there a benefit to allowing any bracket type in macro bodies (i.e. after the =>), instead of just standardizing the curly braces?

One of the people who I have already asked about this guessed that this was an early spontaneous design decision, and it was kept because of backwards compatibility concerns. Is this true?

Well. there is one subtle difference with different brackets on the call site, described here: https://blog.m-ou.se/writing-python-inside-rust-4/#context-dependent-macro
In short (if I understand correctly): in statement positions, macro calls with curly braces are parsed as statements, while other macro calls are parsed as expressions (i.e. only a part of statement), which allows for some compile-time tricks.

Other than that, this is likely a early design decision. I can, however, understand it: for me, it's much more natural, for example, to write vec![1, 2, 3] then vec!(1, 2, 3), when we have similar syntax for arrays; on the other hand, print!("{}", val) is like a function call and will look clumsy as print!["{}", val]; and macros expanding to items, like lazy_static!, are naturally associated with blocks.

2 Likes

Oh yeah, I've seen that subtle difference before. It's really only a subtle difference though; bracket types are still very loose.

I think I was misunderstood in the post:

for me, it's much more natural, for example, to write vec![1, 2, 3] then vec!(1, 2, 3) , when we have similar syntax for arrays; on the other hand, print!("{}", val) is like a function call and will look clumsy as print!["{}", val] ; and macros expanding to items, like lazy_static! , are naturally associated with blocks.

I'm of the exact same opinion here. I'm of the opinion that macros should use the appropriate bracket type depending on where they're used in. Furthermore, I'm of the opinion that macro invocation should only use the appropriate bracket type - not be allowed to use whichever bracket type the user deems better: because why should we want vec!()/vec!{} or lazy_static![]/lazy_static!()?

Basically, this is how I would expect the macro system to work if I had no prior experience with Rust's specific implementation of it:

// 			          v   (1) Curly braces here...
macro_rules! my_macro {

//  v       v   (3) **arbitrary** bracket type here...
    ( <args ) => { // <-- (2) curly braces here...

    } // <-- (2) ...and here
} // <-- (1) ...and here

// NOTE!!! In the macro invocation, the same bracket type would need be used as in the definition:
my_macro!()
//       ^^   (3) ...same as above

Why would I do it like this?

  1. Curly braces around the macro definition - Rust already works like this today, which I find entirely logical. Curly braces should encode code blocks and control constructs, and they do here.
  2. I would also put curly braces around the code blocks in the macro definitions, instead of allowing arbitrary bracket types. For one, this would be consistent with the language's usual use of curly braces. And note that with this I'm not forcing macros to be invoked with curly braces always! The bracket type around the macro definition body is an implementation detail and has no impact whatsoever, so it should be standardized to the obvious choice - curly braces.
  3. I would enforce the bracket types used in macro invocation to be the same as written in the macro definition. It seems logical to me - if the macro author used square brackets around the parameters in the macro definition, the macro should be used with square brackets in its invocation too.

I would really love to hear some concrete points made against (2) and (3). As far as I can tell, there are no drawbacks whatsoever (apart from backwards-compatibility; code like vec!() or lazy_static![] or println!{} wouldn't work anymore)

What if an author wanted to let the user choose?

And note that the backwards-incompatibility is the only drawback that even needs mentioning, as it's a show-stopper.

What if an author wanted to let the user choose?

Can you give an example of a macro use case where this is desirable?

And note that the backwards-incompatibility is the only drawback that even needs mentioning, as it's a show-stopper.

I realize that. My question is why the macros weren't designed this way in the first place

What if an author wanted to let the user choose?

Also, my idea wouldn't keep macro authors from doing this. In fact, it allows them more precise control. For example, if someone writes a macro to initialize arrays, and they want it to work both with curly braces and square brackets, they could just do this

macro_rules! my_array_initializer {
	[ ... ] => {
		// The actual code
	};
	{ ... } => { my_array_initializer![ ... ] };
}

my_array_initializer![1, 2, 3]; // works
my_array_initializer! { 1, 2, 3 }; // works
my_array_initializer!(1, 2, 3); // invalid
1 Like

Well, it probably could be edition-gated, if language developers decide it necessary.

In the end I don't care about changing this in the language. I mainly want to know why it works like it does right now.

I can think of two possibilities:

  1. It was spontaneously decided early on in Rust's life, and kept since, because nobody complained
  2. It was decided after an in-depth discussion about the topic

What was it; was it 1? Or was it 2?

And in case it's 2 I would love to read through those discussions

The best place to look for this sort of historical discussion is in the RFCs. #378 puts in place the expression/statement interpretation of macro brackets, so it’s probably a good place to start.

I see.

Alright so to finally answer my own question: nobody in here knows. I will have to do my own research to find whether there were discussions about this topic, and why exactly this loose approach was decided upon.

2 Likes

Good luck! Please let us know the outcome of your research.

I suspect that this was simply a decision by Graydon Hoare, the original creator of Rust. If so, the pre-decision conversations may all have been in his head.

5 Likes

Some early changes to the macro syntax were proposed in the old wiki in mid-2011. Unfortunately I can't find any record of contemporaneous discussions of this proposal, aside from one brief mention in the rust-dev mailing list archives.

Some historical discussion in the 2012-05-15 and 2012-07-10 and 2012-07-31 weekly meetings. This bit is interesting:

  • pauls: also will want to change the delimeters from [] to {} I believe, not possible with regular expressions
  • pauls: proposal is ident!( expr* ){tt} where the parenthesized exprs and the token trees are optional, so fmt would become fmt!("foo", bar, zed)
  • pauls: this allows for three forms
    • function-call like: fmt!(...)
    • exotic: let!{...}
    • block-like: for!(...) { ... }
  • graydon: feels to me like simplifying expression list all the way down to token trees with a comma-separated expression list as a particular pattern... it doesn't feel like it's that important to me to have a form that parses expressions ahead of time vs the complexity of multiple invocation forms, we could then allow arbitrary delimeters (fmt![] etc)
  • pauls: the simplicity of that appeals to me
  • eholk: current system allows () or []
  • graydon: this seems undesirable
  • pauls: was intended to be a temporary thing
  • graydon: best seems to me to be that each macro has a particular delimeter chosen by the macro author, just from a simplicity perspective of explaining this to the user

But it seems the current system of flexible delimiters was fully implemented shortly after this, and this line of discussion was basically dropped. (Perhaps the situation we ended up in was “good enough” that it never became again became a high-priority issue before the push to 1.0.)

3 Likes

As someone who experience of writing Rust macros is almost zero it did seem a bit odd to allow various bracketing, but at the end of the day does it actually cause any harm or confusion?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.