-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
Simple postfix macros #2442
Conversation
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 `?`.
I'm torn on this topic, but ultimately feeling favorable. On the one hand, sometimes I think "why don't we just make 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 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 |
text/0000-simple-postfix-macros.md
Outdated
|
||
```rust | ||
macro_rules! log_value { | ||
($self:self, $msg:expr) => ({ |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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) => ({
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@petrochenkov Fixed.
There was a problem hiding this comment.
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.
cc @nrc who hated the idea |
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; Considering some other use cases:
Some wilder considerations, which are mostly contra-factual musings of mine...
|
Bit 👍 from me. This would allow for |
I noticed that
As an alternative could you cover the advantages of |
I didn't find this in the RFC but would be worth calling out: is the expectation that |
I think I like this idea, especially given the
I suspect the tension between these two is a big reason we don't have postfix macros yet. |
Even just (And the trait for |
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.
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. |
Reposting a comment that got hidden: It's not just 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 |
Add an alternative for `$self` and explain the upsides and downsides.
@durka Why couldn't it print Does that make sense? |
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 |
@durka I understand what you mean, and that thought crossed my mind too. On the other hand, it can't match |
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 But the feature also looks very useful and simple. |
Non-postfix macros can do the same. In both cases, I think the
I definitely wouldn't expect that to happen, any more than I'd expect
Thanks! |
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 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
Even better, the
Which saves me a few lines and some annoying nesting. You can emulate this currently with an identity |
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? |
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.
It'd still be inappropriately verbose for its prevalence, even if |
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 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. |
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 |
Ergo the "if they so desired" qualifier. |
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. |
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? |
@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 |
As @NyxCode mentioned in the Would it be possible to add
Even if |
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. |
@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 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
} |
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 |
There was a problem hiding this comment.
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.
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)`). |
There was a problem hiding this comment.
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.
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). |
There was a problem hiding this comment.
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.
I'm suddenly looking forward to this feature, because I find it seems useful for simplifying optional parameters in macros. In info!(target: "telemetry", username, ip, options = login_options; "user login to {}", platform);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ key-values Some other log crates may provide more optional parameters like the info!("user login to {}", platform).target!("telemetry").kv!(username, ip, options = login_options); |
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 postfixmacros like
.dbg!()
more useful.Rendered