-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Simple postfix macros #2442
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
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! |
|
@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-valuesSome other log crates may provide more optional parameters like the info!("user login to {}", platform).target!("telemetry").kv!(username, ip, options = login_options); |
|
@rust-lang/lang I would like to propose that you |
|
Here's a practical example where this would be useful: ros2-rust/ros2_rust#460 or https://github.com/ros2-rust/ros2_rust/pull/459/files. It would be much nicer to be able to call |
|
@joshtriplett do you plan on getting back to this? If not, I'd be interested in making a new RFC based on this + comments here that haven't yet been incorporated. |
|
The proposal I'd personally like to see would be for what I've been calling "simplest postfix macros". The macro matcher's first fragment would be This would eliminate much of the complexity of this RFC and its expressiveness limitations. I and others had concerns about those both (e.g.), and it was these kind of concerns that stalled it out. Of course, there remain other concerns about whether it would be too surprising, from a user perspective, to give the macro this evaluation control. Personally, I think that'd be OK, but I know some have concerns there too, and that would need to be discussed at length in any proposal along these lines. Footnotes
|
| In the following expansion, `k#autoref` represents an internal compiler feature | ||
| within pattern syntax (which this RFC does not propose exposing directly), to | ||
| invoke the same compiler machinery currently used by closure captures to | ||
| determine whether to use `ref`, `ref mut`, or a by-value binding. |
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've been thinking about this in the context of the Field Projections proposal, and this k#autoref thing has a fundamental limitation that boils down to: "postfix things want to act on places, and trying to use a reference/ptr instead of a place is restrictive".
- Assignment doesn't work:
macro_rules! write {
($self:self, $val:expr) => ({
$self = $val;
$self
})
}
let mut x;
let _ = x.write!(Some(42)).take();Here if the macro captures &mut x we'd get a borrowck error, despite the fact that the obvious token substitution works.
Also note how returning the $self is also an issue: for the k#autoref to make sense it can't change the type of $self, so I imagine the actual desugaring would be like for closures: we capture captured_x = &mut x, and then use *captured_x in place of $self everywhere. But this prevents returning $self like I'm doing here since that would cause a move (or here a very surprising copy).
- Custom ptr autoref:
macro_rules! call {
($self:self, $name:ident, $($args:expr),*) => ({
$self.$name($($args),*)
})
}
let x: Rc<Foo> = ...;
// Assume `method` takes `RcRef<Self>`:
x.a.method(); // the goal of field projections is to make this work
x.a.call!(method); // for this to work, `k#autoref` should infer the right custom pointerIt's not impossible to infer the right custom pointer here since there's a single call and autoref has to know how to do that, but for multiple ones we get in trouble:
macro_rules! do_two_things {
($self:self) => ({
// Assume `method` takes `RcRef<Self>` and `other_method` takes `&self`.
$self.method();
$self.other_method();
})
}
let x: Rc<Foo> = ...;
x.a.do_two_things(); // there's no "most general type" with which to autoref `x.a` here.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.
Talked to @Nadrieril in detail about this on a call. More details soon, but one key insight Nadri pointed out: closure capture was the wrong model for autoref, and the right model is something more like method receivers. That would also solve case (2).
|
@jplatte wrote:
Currently getting back to this together with @Nadrieril. |
|
That's a lot of pressure 🙈. Let's chat @jplatte, curious to hear your take! |
|
Having indeed chatted with Josh, my feeling is nuanced about this proposal. One important ergonomic requirement is that users should never have to write What's clear to me regardless is that we should start an experiment to implement the just-paste-tokens proposal, so we can get a feel for it and see what ppl come up with to inform further proposals. |
| The use of `match` in the desugaring ensures that temporary lifetimes last | ||
| until the end of the expression; a desugaring based on `let` would end | ||
| temporary lifetimes before calling the postfix macro. |
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.
Even with match, I don't think the temporary lifetimes for arguments would quite be the same as with normal method calls, since match arms are temporary scopes. For example, in
macro_rules! wrap_method {
($self:self, $arg:expr) => { self.method($arg) }
}the temp() in receiver.wrap_method!(&temp()) would have a shorter lifetime than the temp() in receiver.method(&temp()); the former would be dropped at the end of the match arm in the desugared macro expansion, whereas the latter would be dropped at the end of the enclosing temporary scope (allowing it to be used in method chains, e.g.). This could be worked around with new scoping constructs, though. For a very direct solution, there could e.g. be a way to mark match arms as not being temporary scopes. For something less purpose-built, it could maybe use super let to scope the macro expansion body's temporaries as if it was written in place of the match1.
Maybe also of note: wrapping postfix macro expansions in match means they can only expand to value expressions. This is fine for method-call-like macros and postfix .match!, since those both represent values, but unfortunate for field/projection-like macros. x.proj!() wouldn't be usable as a place alias; instead, it would evaluate to a temporary when used in a place expression context.
Similarly, although postfix macros could be written to allow their arguments on the right of the macro invocation to participate in temporary lifetime extension, there's no direct way to allow the "receiver" to be lifetime-extended. This is fine for method-call-like macros (which don't need lifetime extension) and postfix .match! (which only needs to lifetime-extend its arms), but for field/projection-like macros, it could be nice to to have the temporary lifetime extension behavior projection expressions do: in an extending context, &temp().field lifetime-extends the temp() being projected from, so ideally I think it should be possible to write a proj! macro such that &temp().proj!() would also lifetime-extend the temp().
Footnotes
-
I think
super letas implemented today may not quite be able to express this, but in my opinion it's something it should be able to do. Temporary lifetime extension for blocks rust#146098 e.g. would be one way of getting the missing functionality. ↩
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