rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Simple postfix macros

Open joshtriplett opened this issue 6 years ago • 184 comments

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

joshtriplett avatar May 15 '18 07:05 joshtriplett

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.

nikomatsakis avatar May 15 '18 08:05 nikomatsakis

cc @nrc who hated the idea

petrochenkov avatar May 15 '18 10:05 petrochenkov

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 #2361 and #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!()

Centril avatar May 15 '18 13:05 Centril

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)).

est31 avatar May 15 '18 13:05 est31

@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.

joshtriplett avatar May 15 '18 17:05 joshtriplett

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.

dtolnay avatar May 16 '18 01:05 dtolnay

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 )?

dtolnay avatar May 16 '18 01:05 dtolnay

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.

durka avatar May 16 '18 01:05 durka

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.)

scottmcm avatar May 16 '18 02:05 scottmcm

@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.

joshtriplett avatar May 16 '18 03:05 joshtriplett

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 avatar May 16 '18 03:05 durka

@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?

joshtriplett avatar May 16 '18 04:05 joshtriplett

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 avatar May 16 '18 04:05 durka

@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.

joshtriplett avatar May 16 '18 04:05 joshtriplett

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 avatar May 16 '18 04:05 nielsle

@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!

joshtriplett avatar May 16 '18 04:05 joshtriplett

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!{}?

durka avatar May 16 '18 05:05 durka

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.

tmccombs avatar May 16 '18 05:05 tmccombs

@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.

joshtriplett avatar May 16 '18 06:05 joshtriplett

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.

anowell avatar May 16 '18 08:05 anowell

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 an impl block?

That would be highly misleading.

joshtriplett avatar May 16 '18 09:05 joshtriplett

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.

ssokolow avatar May 16 '18 09:05 ssokolow

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)
);

iSynaptic avatar May 16 '18 12:05 iSynaptic

cc @Geal who might find this RFC interesting re: nom

iSynaptic avatar May 16 '18 12:05 iSynaptic

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));

phaylon avatar May 16 '18 13:05 phaylon

Since this rfc allows for :expr as self, would this allow for { /* stuff */ }.do_while!(cond)?

bbatha avatar May 16 '18 15:05 bbatha

@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")

ExpHP avatar May 16 '18 15:05 ExpHP

@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.

joshtriplett avatar May 16 '18 16:05 joshtriplett

We can teach rustdoc (and RLS) that a search for Type::macroname always turns up macroname!, regardless of Type.

durka avatar May 16 '18 17:05 durka

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.

withoutboats avatar May 16 '18 18:05 withoutboats