Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple postfix macros #2442

Open
wants to merge 36 commits into
base: master
Choose a base branch
from

Conversation

joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented May 15, 2018

This RFC introduces simple postfix macros, of the form expr.ident!(),
to make macro invocations more readable and maintainable in
left-to-right method chains.

In particular, this proposal will make it possible to write chains like
computation().macro!().method().another_macro!(), potentially with ?
interspersed as well; these read conveniently from left to right rather
than alternating between the right and left sides of the expression.

I believe this proposal will allow more in-depth experimentation in the
crate ecosystem with features that would otherwise require compiler
and language changes, such as introducing new postfix control-flow
mechanisms analogous to ?.

Update: I've rewritten the desugaring to use the same autoref mechanism
that closure capture now uses, so that some_struct.field.mac!() works.
I've also updated the specified behavior of stringify! to make postfix
macros like .dbg!() more useful.

Rendered

This RFC introduces simple postfix macros, of the form `expr.ident!()`,
to make macro invocations more readable and maintainable in
left-to-right method chains.

In particular, this proposal will make it possible to write chains like
`future().await!().further_computation().await!()`, potentially with `?`
interspersed as well; these read conveniently from left to right rather
than alternating between the right and left sides of the expression.

I believe this proposal will allow more in-depth experimentation in the
crate ecosystem with features that would otherwise require compiler
changes, such as introducing new postfix control-flow mechanisms
analogous to `?`.
@nikomatsakis
Copy link
Contributor

I'm torn on this topic, but ultimately feeling favorable.

On the one hand, sometimes I think "why don't we just make foo.bar!() shorthand for bar!(foo, ...)", which I gather is roughly what you describe here. Except this proposal is opt-in, which is good (see below).

On the other hand, I think there is no fundamental reason that macro-expansion can't be interspersed with type-checking, a la Wyvern or (I think?) Scala. In that case, we could make foo.bar! a true type-based macro invocation, and enable some other nifty things (like selecting which macro to use from context, as Wyvern does).

On the gripping hand, that is so far out as to be science fiction, and the need for postfix macros is real today. Plus, if we ever get there — and indeed if we ever want to get there — I suppose that the $self:self notation could be deprecated and some other notation introduced for a type-based macro.


```rust
macro_rules! log_value {
($self:self, $msg:expr) => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just adding a new matcher self would probably be enough - $anything: self.
During matching it wouldn't match anything except for a "method receiver" in expr.my_macro!(...), but during expansion it would work as expr.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚲 🏠: I think in order to be consistent with function syntax it should be $self as in ($self, $msg:expr) => ({.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petrochenkov That's exactly what I intended. I described the :self as a new "designator", because the Rust documentation used that term. Do you mean something different when you describe it as a "matcher"?

@est31 I considered that possibility; however, in addition to the inconsistency of not using a descriptor, that would limit potential future expansion a bit. Today, you can write $self:expr and use $self, without the compiler attaching any special meaning to the use of the name self as a macro argument. So, making $self without a descriptor special seems inconsistent in a problematic way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshtriplett
I see, the RFC never shows an example with $not_self: self, so I thought that $self is mandatory.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petrochenkov Fixed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshtriplett good point. Ultimately I don't care much about the actual syntax.

@petrochenkov
Copy link
Contributor

cc @nrc who hated the idea

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 15, 2018
@Centril
Copy link
Contributor

Centril commented May 15, 2018

Putting aside some of the more technical aspects of the RFC and focusing solely on the motivation...

This is quite a beautiful RFC. I wholeheartedly support some form of postfix macros;
They make macros feel like methods, which is quite lovely.

Considering some other use cases:

  • dbg! -- here a postfix macro would allow you to just have expr and then (proviso that the precedence checks out) simply tack on .dbg!() at the end and get expr.dbg!(). To me, this is the optimal light-weight debugging experience you can get in the playground. Of course, when you have something like x + y you can always switch back to dbg!(x + y). That said, for expr.dbg!() to work well per designs in Simpler alternative dbg!() macro #2361 and RFC: Quick dbg!(expr) macro #2173, then stringify! needs to behave differently than specified in the RFC right now.

  • throw! -- you can simply write myError.throw!(); This invocation style makes quite a bit of sense; you are taking an object, and then performing some verb on it actively.

Some wilder considerations, which are mostly contra-factual musings of mine...
In a parallel universe where we started out with postfix macros, one might have considered (keeping in mind the precedence issues...):

  • expr.return!()
  • expr.break!('label)
  • expr.break!()

@est31
Copy link
Member

est31 commented May 15, 2018

Bit 👍 from me. This would allow for expr.unwrap_or!(return) which I wanted to do so often but couldn't (as well as expr.unwrap_or!(continue)).

@joshtriplett
Copy link
Member Author

@Centril, @est31: Those are exactly the kinds of things I had in mind. I was partly inspired by recent discussions around try blocks and throw/fail; having postfix macros allows for a lot more experimentation prior to deciding something needs elevating to a language built-in.

@dtolnay
Copy link
Member

dtolnay commented May 16, 2018

I noticed that $self by itself is currently not valid syntax (as of rust-lang/rust#40107).

error: missing fragment specifier
 --> src/main.rs:2:11
  |
2 |     ($self, rest) => {}
  |           ^

As an alternative could you cover the advantages of $self:self over just $self? Is it about future compatibility, consistency with how other fragments are specified, some sort of ambiguous edge case, etc? I think people are generally used to and like the idea of self not needing a type specified in method signatures.

@dtolnay
Copy link
Member

dtolnay commented May 16, 2018

I didn't find this in the RFC but would be worth calling out: is the expectation that $self:self must be followed by either , or )?

text/0000-simple-postfix-macros.md Outdated Show resolved Hide resolved
text/0000-simple-postfix-macros.md Outdated Show resolved Hide resolved
text/0000-simple-postfix-macros.md Outdated Show resolved Hide resolved
text/0000-simple-postfix-macros.md Outdated Show resolved Hide resolved
@durka
Copy link
Contributor

durka commented May 16, 2018

I think I like this idea, especially given the await motivation, but I can't decide about the evaluation issue.

  • If the self argument is not pre-evaluated, it's quite surprising for users: reading a method chain and encountering a macro, you have to revise your understanding of the whole expression so far, like a garden path sentence.
  • If the self argument is pre-evaluated as proposed, it's surprising and limiting for macro authors: normally, a macro gets everything unevaluated and can do as it wishes. Macros like launch_missiles().just_kidding!(); (or more practically, let's say install_drivers().only_if_cfg!(windows);) can't be written.

I suspect the tension between these two is a big reason we don't have postfix macros yet.

@scottmcm
Copy link
Member

Even just .unwrap!() would mean better locations, without needing #2091...

(And the trait for ? would let it work over Result and Option with the same macro expansion.)

@joshtriplett
Copy link
Member Author

joshtriplett commented May 16, 2018

@dtolnay

If the self argument is not pre-evaluated, it's quite surprising for users: reading a method chain and encountering a macro, you have to revise your understanding of the whole expression so far, like a garden path sentence.

Thanks, that's the perfect explanation for what I was trying to get at. I'm going to incorporate that into a revision of the RFC.

If the self argument is pre-evaluated as proposed, it's surprising and limiting for macro authors: normally, a macro gets everything unevaluated and can do as it wishes. Macros like launch_missiles().just_kidding!(); (or more practically, let's say install_drivers().only_if_cfg!(windows);) can't be written.

That's why I'm specifically positioning this as "simple postfix macros". This doesn't preclude adding more complex postfix macros in the future, but it provides a solution for many kinds of postfix macros people already want to write.

@durka
Copy link
Contributor

durka commented May 16, 2018

Reposting a comment that got hidden:

It's not just stringify -- it's any macro that expects an unevaluated expression.

macro_rules! is_ident {
    ($i:ident) => { true };
    ($e:expr) => { false }
}

macro_rules! log {
    ($self:self) => {{
        println!("{:?}", is_ident!($self));
        $self
    }}
}

42.log!();

What does this print? It seems quite surprising for it to print true, but impossible for it to print false.

@joshtriplett
Copy link
Member Author

joshtriplett commented May 16, 2018

@durka Why couldn't it print false? My inclination would be for expr or tt to match, but no other designator. (And another postfix macro could capture it as another $self:self, if the first macro wrote $self.othermacro!().)

Does that make sense?

@durka
Copy link
Contributor

durka commented May 16, 2018

It makes sense when you put on compiler-colored glasses, knowing that it's expanded to this invisible temporary binding. But normally macros can tell that xyz is an :ident and 42 is a :literal, etc, so it's weird that this ability is lost with postfix macros.

@joshtriplett
Copy link
Member Author

joshtriplett commented May 16, 2018

@durka I understand what you mean, and that thought crossed my mind too. On the other hand, it can't match :literal or :ident, because then someone might try to match it that way and use the result in a context that requires a literal or ident.

@nielsle
Copy link

nielsle commented May 16, 2018

Some users will be confused that a postfix macro can change the control flow even though it looks like a method. This can lead to obfuscated code. I can imagine debugging code where my_log.debug!("x {:?}",x) sometimes returns an Err(Error).

But the feature also looks very useful and simple.

@joshtriplett
Copy link
Member Author

joshtriplett commented May 16, 2018

@nielsle

Some users will be confused that a postfix macro can change the control flow even though it looks like a method.

Non-postfix macros can do the same. In both cases, I think the ! in the macro name calls sufficient attention to the possibility that it might do something unusual. And prospective macro writers should use caution when writing control-flow macros to avoid surprising behavior.

I can imagine debugging code where my_log.debug!("x {:?}",x) sometimes returns an Err(Error).

I definitely wouldn't expect that to happen, any more than I'd expect debug!("x {:?}", x); to sometimes return from the calling function.

But the feature also looks very useful and simple.

Thanks!

@programmerjake
Copy link
Member

Also, postfix macros would make every macro that depends on state nicer to use (obviously). For example, something like:

let result = db.query!("SELECT * FROM foo where bar = {bar}");

I have that exact problem in the hardware description language I've been building:
https://salsa.debian.org/Kazan-team/rust-hdl/-/blob/326abb7b71a3caf6e9181364598b5ea79daa0d66/examples/blinky.rs#L17

everywhere I want a HDL expression, currently I have to write:

val!(m, if overflowed { 0 } else { counter + 1 })

it'd be much nicer if I could write:

m.val!(if overflowed { 0 } else { counter + 1 })

@danielrab
Copy link

another example of something that would become significantly nicer to use if we had postfix macro is write!

f.write!(...)

instead of

write!(f, ...)

which we have currently.
In general it makes to code more readable for any macro that has a main thing it operates on and then some arguments.

@danielrab
Copy link

one potential downside I can see though is cluttering of autocompletion suggestions. But I guess we could just not include those in the suggestions.

@Victor-N-Suadicani
Copy link

Didn't see anyone else point this example out but just wanted to pile on another possible use case:

I want to log the variants of an error enum at different log levels (some errors are expected and fine, others are more serious). This has lead me to implement a fn log_level(&self) -> log::Level method on my errors so I can log them like this:

let request = { ... };
if let Err(e) = some_fallible_function(&request) {
    log!(e.log_level(), "An error occurred for {request}: {e}");
}

I would love to just be able to do e.log() and call it a day, but unfortunately I can't get the surrounding context or add any special-case message. However, with post-fix macros I could do it easily:

if let Err(e) = some_fallible_function(&request) {
    e.log!("An error occurred for {request}: {e}");
}

Even better, the log crate (or myself) could implement an extension trait to Result which would allow this:

let value = some_fallible_function(&request).log_err!("An error occurred for {request}: {e}")?;

Which saves me a few lines and some annoying nesting. You can emulate this currently with an identity .map_err which also performs a log but it's a lot more verbose.

@gilaroni
Copy link

gilaroni commented Dec 7, 2022

So, I'm not a huge fan of macros, and try to avoid them when reasonably possible (both implementing, and using them), but sometimes they are by far the best solution.

I strongly believe postfix macros would increase the problem space where macros are the ideal option.

Some people seem to think, that this is exactly a reason to not implement postfix macros, but I disagree with that. Macros are sometimes the best (or even only) solution, which is the reason, why they were implemented in Rust – so why cripple them?

It might make code a little more elegant, but the difference for programmers overall is going to be tiny and marginal.

I feel expressiveness and ergonomics are a big problem for people that use Rust as a general purpose language (instead of just as a replacement for C). And improving expressiveness and ergonomics seems to be an important goal of the Rust leadership, at the moment.

Even the small addition of format argument interpolation felt like a huge win to me in this regard.

I believe postfix macros could open many opportunities that would result in significant improvements in regard to expressiveness and ergonomics.

For example, let's take eyre::WrapErr (a trait that can be used to add additional information to errors):

I'd much rather use:

let content = std::fs::read(path).wrap_err!("Failed to read file {path}"))?;

...than:

let content = std::fs::read(path).wrap_err_with(|| format!("Failed to read file {path}"))?;

This might look like a small improvement, but considering how often error handling is required, and how important proper error handling is, I feel something like this would be a huge improvement (and I'm sure, there are other ways how postfix macros could improve error handling ergonomics).

Besides that, I believe, postfix macros would make it easier and nicer to experiment with new features before they are implemented in Rust.

For example, before the ? operator was added, there was the try macro. Wouldn't it have been significantly better to write:

File::create("my_best_friends.txt")
    .try!()
    .write_all(b"This is a list of my best friends.")
    .try!();

...instead of:

let mut file = try!(File::create("my_best_friends.txt"));
try!(file.write_all(b"This is a list of my best friends."));

?

Also, postfix macros would make every macro that depends on state nicer to use (obviously). For example, something like:

let result = db.query!("SELECT * FROM foo where bar = {bar}");

To be fair, nothing of the above is an earth-shattering improvement.

However, similar to how many small paper cuts can kill a person (or make Rust painful to use), I strongly believe that even a handful of applications of postfix macros could result in a disproportional amount of improvements in regard to ergonomics and expressiveness of Rust.

As I wrote above, even the tiny change of adding format argument interpolation was a huge win for me (I can't wait until we can use full expressions! :)).

Besides that, I think, postfix macros, in some cases, could lead to less code being put into macros.

For example, I'm pretty sure that some procedural attribute macros could be replaced with postfix macros.

So instead of e.g. the whole function being processed by a procedural macro, only a few selected expressions would be targeted by a postfix macro.

Sure, you then also could replace the procedural macro with a function-like macro, but I think many people don't want to do that, because function-like macros disrupt the flow of the code too much. Chainable postfix macros, however, often would be good enough for that.

Maybe it was a mistake to add '?' to the language, since Postfix Macros (.try!) is arguably more appropriate (certainly more general) solution that doesn't involve additional syntax. Should the question mark operator be depreciated?

@ssokolow
Copy link

ssokolow commented Dec 7, 2022

I strongly believe postfix macros would increase the problem space where macros are the ideal option.

Bear in mind that Mr. rust-analyzer himself lists macros as one of the big things that make it difficult to provide good IDE support.

Maybe it was a mistake to add '?' to the language, since Postfix Macros (.try!) is arguably more appropriate (certainly more general) solution that doesn't involve additional syntax. Should the question mark operator be depreciated?

It'd still be inappropriately verbose for its prevalence, even if ? didn't play more nicely with rust-analyzer.

@parasyte
Copy link

parasyte commented Dec 7, 2022

Bear in mind that Mr. rust-analyzer himself lists macros as one of the big things that make it difficult to provide good IDE support.

While true, this also implies that the need for great IDE support has a higher priority than improving the ergonomics of using macros. I'm not certain that holds true in the general case; I can write Rust code without an IDE, but I would feel a terrible burden without macros at all. Given these two extremes, I honestly prefer macros.

Postfix macros would objectively improve ergonomics. For instance, postfix await syntax could have been prototyped with it, giving developers a chance to try-before-they-buy. Some very commonly requested features like keyword/named arguments can also be "hacked into the language" with postfix macros. One could even fake method overloading with them if they so desired.

I know the quote I'm responding to is not arguing against the ergonomics of postfix macros. I just want to shift focus back to why this RFC is so compelling.

@ssokolow
Copy link

ssokolow commented Dec 7, 2022

One could even fake method overloading with them if they so desired.

That wouldn't solve the problem that prevents method overloading from being a core language feature.

The problem being that Rust is consciously designed with a "fearless upgrades" philosophy and method overloading is an API stability footgun because adding a new overload can cause type inference in downstream code to start failing.

No doubt why it typically occurs in languages that began without type inference and now have it being opt-in (eg. C++'s auto), rather than opt-out like Rust.

@parasyte
Copy link

parasyte commented Dec 7, 2022

Ergo the "if they so desired" qualifier.

@tmccombs
Copy link

tmccombs commented Dec 9, 2022

method overloading is an API stability footgun because adding a new overload can cause type inference in downstream code to start failing

That depends on what you mean by "method overloading". If all the overloads have different arity, then it doesn't really cause any problems with inference. And in my experience the way macros are used for function overloading is typically for cases where you each "overload" (which in the case of a macro is actually a specific pattern) has a different number of parameters.

@satvikpendem
Copy link

Would this allow me to transform something like

schema.object::<Pet>("Pet")
    .field!("name", |p| {
        ...
    })
    .field!("age", |p| {
        ...
    });

into

schema.object::<Pet>("Pet", |t| {
    t.field("name", |p| {
        ...
    };
    t.field("age", |p| {
        ...
    };
});

That is, use a sequential builder pattern to then collect all of those macro arguments into a single closure. I'm not sure if this is possible even with postfix macros, is it?

@d4h0
Copy link

d4h0 commented Dec 25, 2022

@satvikpendem: I don't think it would be wise to let postfix macros alter their left-hand expression (and I would be pretty astonished if people who decide if postfix macros are implemented think differently).

I think, the left-hand should have type expr, ident, etc., so that postfix macros can use what is to their left (i.e., no tt), but not alter it (I have never used procedural macros, so I'm not sure, if that would be even possible there).

@d4h0
Copy link

d4h0 commented Jan 15, 2023

As @NyxCode mentioned in the postfix match issue:

Would it be possible to add postfix macros on unstable, to be able to experiment with proposals like postfix match?

.await, ?, and the method chain syntax clearly demonstrate that postfix-based syntaxes can be beneficial. Therefore, it would be advantageous if experimentation with such syntax constructs is made easier on unstable.

Even if postfix macros are never stabilized, they would still be highly useful for such experimentation (and help avoid potential mistakes).

@recatek
Copy link
Contributor

recatek commented Feb 20, 2023

Currently working on (yet another) ECS library in Rust that's heavily generic- and macro-based. I would love to be able to do

world.ecs_query!(component_a, component_b, component_c)

instead of

ecs_query!(world, component_a, component_b, component_c)

especially when this query is part of a long chain.

@daniel-pfeiffer
Copy link

daniel-pfeiffer commented Sep 26, 2023

@kevinushey Before I read this, on the contrary I was assuming that chained macros would be members. I come from a place where I need to add methods that can't currently be expressed as functions. The same goes for object.unwrap_or! { ... } which would only exist for Option and Result.

Maybe we can have it both ways, if this is feasible without rewriting the parser: when there are member macros they take precedence over free macros. And they apply only where implemented. With extended 2.0 syntax:

impl X {
    macro x0($self) { todo!() }
    macro x1($self, $x:expr) { todo!() }
    macro xn($self, ..) { // additional params below
        () => { todo!() },
        ($x:expr) => { todo!() },
        ($x1:ident $x2:tt) => { todo!() },
    }
}
trait Y {
    macro y0($self) { todo!() }
    macro y1($self, $y:expr);
    macro yn($self, ..); // additional params in impl
}

@oli-obk oli-obk added the T-types Relevant to the types team, which will review and decide on the RFC. label Feb 19, 2024
Comment on lines +268 to +271
about that type that the compiler will still enforce). A future RFC may
introduce type-based dispatch for postfix macros; however, any such future RFC
seems likely to still provide a means of writing postfix macros that apply to
any type. This RFC provides a means to implement that subset of postfix macros
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can say with confidence that there is no support for this in T-types.

Furthermore we don't have the infrastructure in the compiler for supporting this, and are not expecting this to be possible within the next 5-10 years. There is some serious reengineering required to get there, and the incremental steps required for it have stalled two years ago.

Comment on lines +279 to +292
Rather than this minimal approach, we could define a full postfix macro system
that allows processing the preceding expression without evaluation. This would
require specifying how much of the preceding expression to process unevaluated,
including chains of such macros. Furthermore, unlike existing macros, which
wrap *around* the expression whose evaluation they modify, if a postfix macro
could arbitrarily control the evaluation of the method chain it postfixed, such
a macro could change the interpretation of an arbitrarily long expression that
it appears at the *end* of, which has the potential to create significantly
more confusion when reading the code.

The approach proposed in this RFC does not preclude specifying a richer system
in the future; such a future system could use a new designator other than
`self`, or could easily extend this syntax to add further qualifiers on `self`
(for instance, `$self:self:another_designator` or `$self:self(argument)`).
Copy link
Contributor

@oli-obk oli-obk Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edit: wrong text section highlighted, so read the comment as a free comment

I think this RFC needs to be evaluated on the basis that such a system will never come to Rust, as that is the likeliest situation we'll find ourselves in afaict.

If this RFC is only accepted because it leaves the door open to a future type based extension, then it should not be accepted imo.

If this section causes opponents of the RFC to accept the RFC as a compromise, because a type based system is expected to come in the future, then this should be rediscussed.

Imo this RFC should explicitly state that we will never get a type based system, and include T-types in the FCP.

Comment on lines +342 to +350
We could omit the `k#autoref` mechanism and only support `self`. However, this
would make `some_struct.field.postfix!()` move out of `field`, which would make
it much less usable.

We could omit the `k#autoref` mechanism in favor of requiring the macro to
specify whether it accepts `self`, `&self`, or `&mut self`. However, this would
prevent writing macros that can accept either a reference or a value, violating
user expectations compared to method calls (which can accept `&self` but still
get called with a non-reference receiver).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just use &$self as an expression and have that evaluate to the tokens that were passed as the self? Just like with other arguments, if you don't want it evaluated twice, first evaluate it into a let binding, then use that twice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-macros Macro related proposals and issues A-method-call Method call syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet