Postfix macros support

With a trivial way in which any custom piping can be implemented for any given type for:

let shared = state
  .to(Mutex::new)
  .to(Arc::new)
  .to(Some);

not having at least somewhat of a similar solution for macros, which necessarily require forced prefix syntax, feels rather out of place - to say the least.

The RFC on the matter has been stuck in a limbo for over 6 years now. The topic has been brought up time after time again. A separate library has been made to cover up for the lack of any significant attention in this regard.

From the discussions I can see so far, there doesn't seem to be any major downside / drawback / issue to the idea itself, either. It's the exact implementation / syntax specifics that different people have different positions on.

Can we attempt to settle them once and for all, instead of sporadically commenting on the topic every once in a while, continuously postponing the underlying specifics to some other time?


Possible Implementation #1:

Do not introduce any additional syntax to the existing macro declarations. Merely allow the macros with one argument or more to be used in a postfix notation - as long as a provided AST node matches the expected argument of the macro itself.

macro_rules! debug {
  ($e:expr) => { dbg!($e); }
}
fn use() {
  let example = 2021.debug!(); 
}

Possible Implementation #2:

Same as #1, yet with a prior #[macro_postfix] annotation, required to use any given macro in its postfix form. Macros not explicitly marked as such will not be processed by the compiler and throw a compile time syntax error, as they do today.

#[macro_postfix]
macro_rules! debug {
  ($e:expr) => { dbg!($e); }
}
fn use() {
  let example = 2021.debug!(); 
}

Possible implementation #3:

Require an explicit $self declaration as a first argument of a given macro, alongside the type of node the macro will be processed for, in its postfix format. As with #2, any macro that doesn't declare an explicit ($self:?) as a first argument will not be processed in a postfix position.

macro_rules! debug {
  ($self:expr) => { dbg!($self); }
}
fn use() {
  let example = 2021.debug!(); 
}

Possible implementation #4:

Similar to #3, yet without an explicit node type for the $self, with an optional :self allowing for a custom name in place of the $self itself.

macro_rules! debug {
  ($self) => { dbg!($self); }
  // same as
  ($e:self) => { dbg!($e); }
}
fn use() {
  let example = 2021.debug!(); 
}

In the spirit of (pre) RFCs, feel free to either :+1: or :-1: the functionality itself (1) and your own preferred implementation of it (2), alongside your line of reasoning on the matter.

If possible, make an example of a project you have personally worked on, wherein having such a feature right would greatly help/streamline/spare you from unnecessary time/effort/cognitive load.

That should be more a great deal more helpful any amount of zero context one-click :heart: - for people who will be implementing this afterwards, should that ever happen, in particular.

I'll try my best to organize the incoming pros/cons in at least somewhat comprehensive of a manner, in the meantime.

2 Likes

If you're proposing language changes, this should be posted to IRLO, not here. Regardless, if you have read the discussions in the RFC issues, you should know that it's more complex than "no major downside/drawback/issue". A major downside is that macros can't interact with the type system in any way and can't use type-based resolution, but methods (which normally use the postfix syntax) do. I may want to add a postfix .unwrap_or!() macro to Option and Result, but adding it to all types would be crazy, and there is no way to avoid it with the current macro system.

You're not making your point stronger when you give such absurd examples.

Hello, I think I have a meaningful example.

I recently wrote a macro to check if a value is in a set of values or ranges: is_in!(value, 1..=16, 19, 21..23) . However, I think the following syntax would look much better value.is_in!(1..=16, 19, 21..23).

I'm sorry, but that's not a good example either.
The first rule of macrology is: never write a macro when a function will do.

A function that could do this can be written like this¹:

use std::ops::Range;

fn contains(
    heaps: &[Range<usize>], 
    needle: usize, 
) -> bool {
    heaps.iter().any(|heap| {
        heap.contains(&needle)
    })
} 

Functions are generally superior to macros when it's a problem that can be solved by a function, because functions have type safety w.r.t. their inputs and output, while as mentioned by @afetisov macros have no notion of the type system, at all.

Aside from that, functions are generally also easier to read and understand.

And finally, macros are just meant to solve a different set of problems than functions do. Macros are intended for syntactic abstraction (often eDSLs or boilerplate-cutting at the syntactic level), and compile-time calculation of data (as opposed to calculating such data at runtime).

And then there's a special case: @dtolnay has written a bunch of almost Clarketech-level macros like the paste, syn, and quote crates that do things that user-defined code shouldn't really be able to do² in Rust (hence the magic Clarketech aspect), but are exceedingly useful at times.

¹ I'm on my phone so I haven't tested whether it actually compiles. But I expect it would, and if it doesn't it's likely due to a silly, and easily fixed, mistake like a typo.
² That's hyperbole - obviously it can be done since the macros exist, but implementing those crates involves knowing a lot of the darker corners of rustc. So not exactly easy to do.

EDIT: just discovered an issue in the example - std::ops::Range<usize> should be replaced with eg std::ops::RangeBounds<usize> to account for all the different range types. Using that is a bit more involved, but is definitely still doable.

EDIT2: Hmm having tried this, there might actually be a point to using a macro here: std::ops::RangeBounds<usize> is not dyn-safe (aka object-safe), which means that polymorphism isn't really available i.e. no masquerading different range types under std::ops::RangeBounds<usize>.
Thus, using it as a type bound would limit every heap to be the same type. A macro would indeed be able to get around that.

2 Likes

So... Surprisingly good example? :slight_smile:

Although this might be more "evidence that Range and friends are difficult to use" than "evidence post-fix macros are needed".

But having skimmed the RFC, postfix macros are tempting!

But this is one for IRLO, we're limited to idle speculation here.

1 Like

It's a good example of using macros to circumvent limitations in the trait system.

But I wouldn't call it a good example for postfix macros, per sé.

So this, yeah.

I can see some use cases where they could be useful, but the technical limitations preventing them from being implemented are not trivial to solve.
So don't hold your breath on them becoming a real feature any time soon :slight_smile:

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.