rfcs
rfcs copied to clipboard
Simple postfix macros
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.
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.
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; 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 haveexpr
and then (proviso that the precedence checks out) simply tack on.dbg!()
at the end and getexpr.dbg!()
. To me, this is the optimal light-weight debugging experience you can get in the playground. Of course, when you have something likex + y
you can always switch back todbg!(x + y)
. That said, forexpr.dbg!()
to work well per designs in #2361 and #2173, thenstringify!
needs to behave differently than specified in the RFC right now. -
throw!
-- you can simply writemyError.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!()
Bit :+1: 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)
).
@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.
I noticed that $self
by itself is currently not valid syntax (as of https://github.com/rust-lang/rust/issues/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.
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 )
?
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 likelaunch_missiles().just_kidding!();
(or more practically, let's sayinstall_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.
Even just .unwrap!()
would mean better locations, without needing https://github.com/rust-lang/rfcs/pull/2091...
(And the trait for ?
would let it work over Result
and Option
with the same macro expansion.)
@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.
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
.
@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?
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.
@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.
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.
@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!
Yes, if we are committed to pre-evaluation, then the passed-through $self
can only match :self
, :expr
, and :tt
.
Can you still use any type of braces? Can I write x.y![]
and x.y!{}
?
I was also considering writing an RFC for postfix macros, but wasn't sure of the details yet. I was happy to see this.
An alternative syntax for the macro definition could be:
macro_rules! log_value {
$self:self.($msg:expr) => ({
...
})
}
I'm not sure I like it more though. On the one hand the pattern looks more like the invocation syntax, and if we later allow lazy evaluation of self, we could just allow other designators for the self argument, like $self:expr
. On the other hand, it is inconsistent with normal methods, where the self argument is inside the parenthesis.
@durka
Can you still use any type of braces? Can I write x.y![] and x.y!{}?
I'm inclined to say "yes", because I can think of macros that would look more appropriate with such delimiters.
It seems to me that the lack of type identification on the self
arg means you can't really get rustdoc to surface it as if it were a method on a type or trait, so it seems like discoverability in documentation is less-than-ideal (a drawback?). Any chance of defining self
macros inside an impl
block? (I expect that is more problematic if supporting self
in proc macros is an eventual goal.)
And fwiw, I somewhat anticipate this getting used to implement "bang-methods" with variable arity and named args. I'm not sure if that's good or bad, but if it becomes common, it seems like it would result in rustdoc feeling less helpful.
On May 16, 2018 1:45:35 AM PDT, Anthony Nowell [email protected] wrote:
It seems to me that the lack of type identification on the
self
arg means you can't really get rustdoc to surface it as if it were a method on a type or trait,
It isn't a method on a type or trait; you could potentially call it on many different kinds of expressions of varying types. So associating it with a type in documentation doesn't make sense.
so it seems like discoverability in documentation is less-than-ideal (a drawback?).
A postfix macro should appear in documentation very much like a non-postfix macro does.
Any chance of defining
self
macros inside animpl
block?
That would be highly misleading.
so it seems like discoverability in documentation is less-than-ideal (a drawback?).
A postfix macro should appear in documentation very much like a non-postfix macro does.
I suspect the concern is that newcomers will expect to see it documented as a member of of whatever type it's being called on and then erroneously conclude that it's not documented at all.
I could see the nom
crate really benefiting from this. Right from the nom
documentation, this example:
named!(hex_primary<&str, u8>,
map_res!(take_while_m_n!(2, 2, is_hex_digit), from_hex)
);
would become a lot easier to read:
named!(hex_primary<&str, u8>,
take_while_m_n!(2, 2, is_hex_digit).map_res!(from_hex)
);
cc @Geal who might find this RFC interesting re: nom
This might also be nice for allowing inline type ascription:
let res = foo(typed!(String: bar.into()));
// could also be
let res = foo(bar.into().typed!(String));
Since this rfc allows for :expr
as self, would this allow for { /* stuff */ }.do_while!(cond)
?
@bbatha the current design very deliberately does not support this, and implicitly evaluates its argument to a temporary binding. The argument is that something.compute_foo()?.another_method().do_while!(true)
can be very confusing to mentally parse. (I like @durka's description of calling it a "garden path sentence")
@ssokolow
I suspect the concern is that newcomers will expect to see it documented as a member of of whatever type it's being called on and then erroneously conclude that it's not documented at all.
I would find it surprising if a documentation search for expr.macroname!()
didn't at some point involve searching for "macroname", if not first.
We can teach rustdoc (and RLS) that a search for Type::macroname
always turns up macroname!
, regardless of Type
.
I know postfix macros are a popular feature, but I am disinclined to merge this RFC, and I think it should be at least postponed until we have more bandwidth to have the larger conversation about language design that it implies.
Method syntax today is a type dispatched resolution system and macros are not today type dispatched. This has always been the reason we have not supported method syntax macros. This RFC proposes to resolve this issue by giving up the connection between method syntax and type dispatched resolution. I think that is a significant loss, and I am not convinced that the usefulness of postfix macros justifies that change to the language. Any conversation about that would be lengthy and involved, and I don't think we can have that conversation now while trying to ship the features necessary for 2018.
I am troubled by the fact that this loss doesn't seem to be reflected in the RFC. The fact that they aren't type based is mentioned briefly in the detailed design, but its not listed in the drawbacks at all. I would think an RFC on this subject would have to center a discussion of why to reverse that position. I'm concerned about the possibility that we are not effectively transmitting knowledge about previous team decisions to new team members in a more general way.
Finally, I don't think await!()
is a strong motivation for this RFC. The async/await RFC has await!()
as a compiler built in to avoid deciding on the final syntax, but await expressions are not macros and can have whatever syntax we like. This RFC does not enable that.