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 15 commits into
base: master
from

Conversation

Projects
None yet
@joshtriplett
Copy link
Member

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
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
and language changes, such as introducing new postfix control-flow
mechanisms analogous to ?.

Rendered

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
`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

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented May 15, 2018

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.

the first argument, and outside any repetition. If the macro includes such a
case, then Rust code may invoke that macro using the method-like syntax
`expr.macro!(args)`. The Rust compiler will expand the macro into code that
receives the evaluated `expr` as its first argument.

This comment has been minimized.

@petrochenkov

This comment has been minimized.

@petrochenkov

petrochenkov May 15, 2018

Contributor

Or maybe not since the macro doesn't accept tokens in this case, only a single interpolated token representing the whole receiver expression, so it may be okay to expand it later.

x.identity1!().identity2!() -> identity2!( $(x.identity1!()) ) -> x.identity1!() -> identity1!( $(x) ) -> x

This comment has been minimized.

@petrochenkov

petrochenkov May 15, 2018

Contributor

Oh, you actually mean "evaluated", not "expanded" according to the log_value example.
I.e. if $self is used twice in a macro m called like complex_expr.m!(...), then complex_expr is evaluated only once and temporary variable with its result is automatically created and that variable is passed to the macro and not the expression itself?

That's something seriously novel and untypical for our macro system.
I'd expect the macro author to do it instead.

macro_rules! log_value {
    ($self:self, $msg:expr) => ({
        let tmp = $self;
        eprintln!("{}:{}: {}: {:?}", file!(), line!(), $msg, tmp);
        tmp
    })
}

This comment has been minimized.

@joshtriplett

joshtriplett May 15, 2018

Author Member

@petrochenkov Normally I'd agree, but I specifically went that route because it avoids any questions about how much of the preceding expression gets passed into the macro un-expanded and un-evaluated, which has been at issue in previous discussions of postfix macros.

I think it'd be really surprising to write something like a().b().c().d().m!() and have m receive the whole preceding a().b().c().d() (and all arguments to those functions), and potentially evaluate it more than once. That holds doubly true if those preceding bits include macros themselves. If I see m!(foo()) I expect that foo() might not get called; if I see foo().m!(), foo seems "outside" the macro, so I think it makes sense to know that foo() will always get called.

I do agree that the evaluation here makes this different than other macro arguments. However, that's part of how I'm positioning this as "simple" postfix macros; it specifically does not try to process tokens for that argument.

This comment has been minimized.

@petrochenkov

petrochenkov May 15, 2018

Contributor

Ok, if macro expansion generates let tmp = $self; automatically (in principle, we can do this because we know that for this syntax both $self input and macro output are expressions), then the macro needs to decide how it consumes the result of evaluation - by value, by immutable reference, by mutable reference.

Manually it can be easily done using existing mechanisms:

let tmp = $self; // consume the receiver
let tmp = &$self; // borrow the receiver
let tmp = &mut $self; // borrow the receiver

but if it's done implicitly, then it needs some extra syntax - $self: &mut self or something.

This comment has been minimized.

@joshtriplett

joshtriplett May 15, 2018

Author Member

@petrochenkov My intention was that the macro doesn't get to decide that; $self has whatever type the "subject" expression does. If the preceding expression has type &mut T then the macro gets a &mut T.

If you get a T and you don't want to consume it, you can always let tmp = &mut $self yourself, and then return $self at the end after you no longer hold that reference.

The one added complication here: if the subject is mutable, should $self be effectively a let mut? That would allow, for instance, writing $self = ...;. I think the answer is yes, and I think the compiler can do that with the generated internal temporary, but it's worth thinking about.

That said: while I'm proposing doing it this way for simplicity's sake, if you think it's straightforward to do this another way, please let me know and I'm open to the possibility. My main argument against doing so is that doing so seems much harder and I don't want to make the perfect the enemy of the good. If doing so seems easy to you, I'd like to take that into account.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

Please put https://github.com/rust-lang/rfcs/pull/2442/files#r188397176 somewhere in the RFC (perhaps in the alternatives section). I was also super surprised to find out that a mere macro matcher can change evaluation order.

This comment has been minimized.

@joshtriplett

joshtriplett May 16, 2018

Author Member

@durka I added a rationale for this design choice to the RFC.

This comment has been minimized.

@ExpHP

ExpHP May 16, 2018

If you get a T and you don't want to consume it, you can always let tmp = &mut $self yourself, and then return $self at the end after you no longer hold that reference.

I would like to see an example of code where such a macro has been expanded (or at least, a faithful attempt at representing the generated AST). I would assume that the macro automatically encloses its output in braces (to allow for the statement that binds $self), which means it is forced to move input lvalues, and thus let it = &$self; does nothing to prevent a.foo!() from moving a.


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

This comment has been minimized.

@petrochenkov

petrochenkov May 15, 2018

Contributor

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.

This comment has been minimized.

@est31

est31 May 15, 2018

Contributor

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

This comment has been minimized.

@joshtriplett

joshtriplett May 15, 2018

Author Member

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

This comment has been minimized.

@petrochenkov

petrochenkov May 15, 2018

Contributor

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

This comment has been minimized.

@joshtriplett

This comment has been minimized.

@est31

est31 May 16, 2018

Contributor

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

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented May 15, 2018

cc @nrc who hated the idea

@Centril Centril added the T-lang label May 15, 2018

@Centril

This comment has been minimized.

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 #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!()
When expanding a postfix macro, the compiler will effectively create a
temporary binding for the value of `$self`, and substitute that binding
for each expansion of `$self`. This stands in contrast to other macro
arguments, which get expanded into the macro body without evaluation. This

This comment has been minimized.

@est31

est31 May 15, 2018

Contributor

Could you make a simple example of how expansion would look like? Just to make it a little bit more clear to the reader.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

I wrote out an example: hopefully this is correct.

macro_rules! log {
    ($self:self) => {{
        $self.log!("value");
        $self
    }};
    ($self:self, $label:expr) => {{
        println!("{}: {:?}", $label, $self);
        $self
    }}
}

fn main0() {
    42.log!();
}

fn main1() {
    {
        let _self = 42;
        {
            _self.log!("value");
            _self
        }
    };
}

fn main2() {
    {
        let _self = 42;
        {
            {
                let __self = _self;
                {
                    println!("{}: {:?}", "value", __self);
                    __self
                }
            }
            _self
        }
    };
}

Does that look right?

This comment has been minimized.

@ExpHP

ExpHP May 16, 2018

(I'd like to copy my comment here that this expansion unconditionally moves lvalue "receivers.")

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

Well, yeah, that's a problem. We could extend it to $self:self, $self:&self, $self:&mut self, but that looks even weirder.

@est31

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member Author

joshtriplett commented May 15, 2018

@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

This comment has been minimized.

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

This comment has been minimized.

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

($e:expr) => ({
// ... Current body of await! ...
})
($self:$self) => (

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

typo here: $self:$self -> $self:self

This comment has been minimized.

@joshtriplett

joshtriplett May 16, 2018

Author Member

Fixed, thanks.


We could define a built-in postfix macro version of `await!`, without providing
a means for developers to define their own postfix macros. This would
also prevent developers from.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

from what?

This comment has been minimized.

@joshtriplett

joshtriplett May 16, 2018

Author Member

Fixed, thanks.

`T`, `&T`, or `&mut T`. The internal binding the compiler creates for that
expression will have that same type.

Calling `stringify!` on `$self` will return `"$self"`.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

What would be the problem with stringifying the evaluated expression? Or the unevaluated one, for that matter.

This comment has been minimized.

@joshtriplett

joshtriplett May 16, 2018

Author Member

The expression doesn't get evaluated until runtime; the compiler just substitutes an internal temporary location. I didn't want stringify! to have undefined behavior or return the name of that internal temporary location, so I specified it explicitly. I've clarified this.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

This is basically another limitation caused by pre-evaluation. expr.dbg!() doesn't work if you can't use stringify. You could make stringify magic but that sidesteps the actual issue.

Also, 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.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

I suppose you could say that within the body of a postfix macro, $self is a totally opaque expression which only matches :expr.

the first argument, and outside any repetition. If the macro includes such a
case, then Rust code may invoke that macro using the method-like syntax
`expr.macro!(args)`. The Rust compiler will expand the macro into code that
receives the evaluated `expr` as its first argument.

This comment has been minimized.

@durka

durka May 16, 2018

Contributor

Please put https://github.com/rust-lang/rfcs/pull/2442/files#r188397176 somewhere in the RFC (perhaps in the alternatives section). I was also super surprised to find out that a mere macro matcher can change evaluation order.

@durka

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

scottmcm commented May 16, 2018

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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!

@durka

This comment has been minimized.

Copy link
Contributor

durka commented May 16, 2018

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

@aturon

This comment has been minimized.

Copy link
Member

aturon commented May 25, 2018

@joshtriplett Thanks -- I feel like we're getting to the bottom of the two different ways of thinking about this space:

  • Macros-are-special. After all, macro usage is always marked with a !, which is plenty of heads-up that you're not making a "normal" function/method call. Because of this marking, and the special capabilities of macros in general, it seems totally fine for things like dispatch rules to be different from. normal functions/methods.

  • Macros-should-be-normal. While there's no doubt that macros have special abilities relative to normal functions/methods, they also have aspects that are orthogonal to these abilities, and are shared with other language features. Namely: scoping and dispatch. (See more below).

I should say that I find both of these perspectives reasonable. But I think it's worth distinguishing them in order to pinpoint the differences in feelings about the proposal.

With Rust 1.0 the macro scoping system was indeed quite distinct from that for normal functions -- but that was seen at the time as a wart, and with the 2018 Edition we hope to finally bring macro scoping into harmony with the rest of the module system, so that you import/rename macros via the usual use mechanism.

Once that happens, the scoping and dispatch for today's macros will work identically to that for free functions: to make an invocation, you need to unambiguously reference the function/macro, which is then dispatched to.

For the macros-should-be-normal perspective, this is great -- it means that macros work just like functions in every respect where that makes sense. And if you take that as the starting point, then this RFC feels like a step backward, because it creates a new split, this time between macro methods and normal methods, that is not connected to the power/abilities of macros, but about "orthogonal" scope/dispatch rules. (I put "orthogonal" in scare quotes here because, from an implementation standpoint, interleaving macro expansion and type-checking is anything but!)


To try to dig in further, what worries me here is:

  • Method dispatch is designed the way it is for a reason! While there's no doubt that we could support method macros as proposed (and doing full dispatch would be very hard), I feel like the discussion here isn't yet grappling with the full design fallout of such a decision. This is part of what I meant about the lang team needing to take a "long term" view: it's clear that there are some nice wins to be had with this syntax, but it's far less clear what it means for API design in the long run.

  • More concretely: there are patterns of API design today that revolve around the way that method dispatch works, e.g. choosing what things to provide as inherent methods, when to use traits, etc. These tend to come with a variety of tradeoffs between flexibility and client ergonomics. When I talk about method dispatch working differently for macros, I'm thinking not just about the core mechanics (which are simple enough), but also the API design patterns that emerge. @joshtriplett raises an interesting prospect that one can introduce a trait along with a method macro to simulate/recover some amount of type dispatch, but the design principles will still diverge -- there's nothing akin to inherent methods, and there's effectively a single shared namespace for method macros (unlike with normal methods, which are mediated through traits/types and therefore allow for coexistence).

For me, none of this translates to an absolute "no". The point is just that the mechanism proposed looks simple, but has complex and hard to gauge implications because of the divergence from standard dispatch. And for me, that suggests that we should:

  • Strengthen the arguments in favor of having method macros in some form, given these potential downsides
  • Explore in somewhat greater depth what we expect typical usage/API patterns to look like
  • Examine the potential for using standard dispatch
@joshtriplett

This comment has been minimized.

Copy link
Member Author

joshtriplett commented May 25, 2018

@aturon Thank you for a well-thought-out analysis.

I do think the distinction you draw makes sense; however, I would suggest unpacking the term "normal" and instead pinpointing specific ways that macros do and don't behave like other things, and which other things it behaves like. Without that, the blanket term "normal" creates a normative expectation that macros should behave like all other similar-looking language constructs, rather than carefully considering and adopting those expectations only when they make sense. (We already expect, for instance, that you can use macros in places you can't call functions, and that evaluation works completely differently, and those things should not change.)

I think we need to separate two important questions. One is "should we do type-based dispatch at all", and the other is "should we only do type-based dispatch". For the former question, I'd say "maybe" with inclinations towards "no". For the latter, though, I'd say emphatically no. Another way to look at it: should we force macros to integrate with the type system and declare what type of thing they can operate on? I'm not suggesting that we should never provide the ability to integrate with the type system; I'm arguing that we should not force macros to integrate with the type system, when part of the point of a macro is to differ in that regard.

When you declare a macro parameter of type expr, it can take any expression. That's not the same thing as a function with a generic type parameter T, because you don't have to declare what facets of T you depend on. You just use features of T and if you call the macro on a type that doesn't have those features you get a compile-time error on the resulting expansion. That's a key distinction between macros and functions, and I'm arguing that we should have the same distinction between macros and methods.

I feel like you're looking at $self solely through a lens of "what type do we dispatch on". I'm looking at it through a lens of "what type is the macro limited to operating on". And to that, my answer is "whatever the expansion will compile with at compile time". Macros are not type-checked, they're effectively duck-typed; you can call them on anything their expansion will work with, and the expansion is type-checked (and everything-else-checked).

Suppose, hypothetically, that we could trivially support "typed" macro parameters, similar to function parameters. You could have a type associated with a macro "expr", and the macro would only accept expressions of that type. If we had such a feature, and didn't have backward compatibility concerns, would we say that you should only have type-restricted macros? Or would we say that it makes sense to give macros the power to operate on arbitrary expressions without declaring their type?

Along the same lines, I'm arguing that there's value in a postfix macro system that does not declare the type of $self. And you can't do pure type-based dispatch without also declaring the type of $self. (You can work around that, through traits and generics, but that's not the same thing as not adding that restriction in the first place.)

As an aside, I do think there'd be value in explicitly stating that this proposal benefits from the proposed changes to macro scoping and export/import.

@aturon

This comment has been minimized.

Copy link
Member

aturon commented May 25, 2018

@joshtriplett

I feel like you're looking at $self solely through a lens of "what type do we dispatch on". I'm looking at it through a lens of "what type is the macro limited to operating on".

One of the things I really like about this proposal is that it doesn't treat $self as something that the macro gets to "operate on" in the normal sense; instead, it's evaluated in just the same way it normally would be for a method call, always. That's what prevents the "garden path sentence" issue, and retains the property that macros only get to "reinterpret" the things that are given inside the macro invocation.

I think, though, that it's this property of the proposal that also makes me feel like we should "go the full distance":

impl MyConcreteType {
    macro_rules! unwrap_or {
        ($self:self, $msg:expr) => ({
            ...
        })
    }
}
@joshtriplett

This comment has been minimized.

Copy link
Member Author

joshtriplett commented May 25, 2018

@aturon But that doesn't "go the full distance", because inside that macro you can still do anything you want to $self, whether MyConcreteType supports it or not. And conversely, that would prevent useful uses of macros that our type system can't express. As a concrete (though silly) example:

macro_rules! assert_matches {
    ($self:self, $p:pat) => {{
        match $self {
            $p => {}
            _ => { /* assertion failure */ }
        }
    }}
}

(Please ignore the question of whether we want this specific macro, and treat it as a simple example of a class of possibilities.)

What type is $self here? "Any type that can be pattern-matched against the pattern $p". That's outside our type system. It gets type-checked after expansion.

Along the same lines, consider a macro that would let you write foo().bar().is!(Some(x), x == y). That's not the most ergonomic syntax, but it'd be a nice way to experiment with some interesting possibilities of a hypothetical "is" operator and the ability to combine if let with boolean if.

Again, I'm not arguing that you shouldn't be able to have type-dispatched macros that only exist for a particular type. I'm arguing that you shouldn't only be able to have type-dispatched macros.

@MajorBreakfast

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 26, 2018

@joshtriplett These macros look awesome! And they could even be chainable 🤤

@Centril Centril referenced this pull request May 26, 2018

Open

Type ascription (tracking issue for RFC 803) #23416

1 of 6 tasks complete
@warlord500

This comment has been minimized.

Copy link

warlord500 commented May 26, 2018

actually, after thinking about it for a while, the only limitation, you have with post fix macros is that you can't put $self in a closure to make self not be pre-evaluated.

@aturon

This comment has been minimized.

Copy link
Member

aturon commented May 29, 2018

@joshtriplett

But that doesn't "go the full distance", because inside that macro you can still do anything you want to $self, whether MyConcreteType supports it or not.

I don't quite follow this. Assuming type-based dispatch, anything you did with $self in the macro that was not type correct would result in a type error on use of the macro. (This is different from with normal methods, of course, but is a separate issue from dispatch).

And conversely, that would prevent useful uses of macros that our type system can't express.

That's not necessarily true. You could imagine:

trait AssertMatches {
    macro assert_matches($self:self, $pat:pat);
}

impl<T> AssertMatches for T {
    macro assert_matches($self:self, $pat:pat) { ... }
}

Of course, bringing this into the realm of traits opens yet another can of worms :-)

Anyway, I don't think we need to go deeper down this rabbit-hole just at the moment, and I think everyone would agree that type-dispatched macros are a looooong way off, if we ever did adopt them.


On the whole, having sat with this proposal for a bit (and thanks to the discussion), I'm feeling increasingly comfortable with it. What I'd most like to see is a larger collection of worked examples, to get a feel for the impact on API design.

@aturon

This comment has been minimized.

Copy link
Member

aturon commented May 30, 2018

Procedural note: one thing that's a bit tricky here is that multiple Lang Team members have reservations about this direction, but lack the time to fully engage on the thread to turn those intuitions into more concrete, actionable concerns.

I think everyone agrees that this is out of scope for Rust 2018, and we've postponed a fair number of other RFCs on that basis, but haven't really set a clear policy (or declared an "impl period"). Moving to postpone feels like "stop energy", but the discussion here is somewhat of a distraction/source of stress for some folks on the team.

@joshtriplett, do you have thoughts on how best to proceed here? I think the discussion so far has laid out some of the basic contours, but before we can go much further we'd need to hear more from others on the team. Would you be open to postponing until after we cut the Edition?

@durka

This comment has been minimized.

Copy link
Contributor

durka commented May 30, 2018

@tmccombs

This comment has been minimized.

Copy link

tmccombs commented May 30, 2018

My understanding wasn't so much that we need to have this for await, as that try! and await! both would have benefited from this, and there are probably a lot of other macros that would/will benefit from postfix syntax.

@alexreg

This comment has been minimized.

Copy link

alexreg commented May 30, 2018

I don’t understand the fixation on Rust 2018. Surely it doesn’t hurt to have a go at a medium-priority feature like this, and if it’s not ready for stabilisation by the end of 2018, so be it (unlikely anyway).

@aturon

This comment has been minimized.

Copy link
Member

aturon commented May 30, 2018

@alexreg It's all about focus. We have a lot of stuff in flight that we're trying to get polished up to ship with Rust 2018 (only a few months away), and at this point are primarily cutting scope, not widening it.

While no single RFC eats that much time, collectively they can be a drain on the bandwidth of the various teams responsible for review. This is part of why we had the impl period last year. We haven't taken that step this year, but the basic issues are the same.

@alexreg

This comment has been minimized.

Copy link

alexreg commented May 30, 2018

@aturon Fair enough then. I suppose it depends who tackles this. Also, seems I was mistaken about the Rust 2018 deadline. :-)

@tmccombs

This comment has been minimized.

Copy link

tmccombs commented Jun 3, 2018

Just throwing out another use case. This would allow defining something like:

macro_rules! pipe {
  ($self:self, $f:path($args:tt) => { $f($self, $args) }
  ($self:self, $m:path!($args:tt) => { $m!($self, $args) }
}

that would allow postfix style calling of free functions and macros that aren't defined to take self (using a differnt syntax than method calls).

@Pauan

This comment has been minimized.

Copy link
Member

Pauan commented Jun 3, 2018

@tmccombs An example of usage:

// Equivalent to corge(qux(bar(foo, 1, 2), 3))
foo.pipe!(bar(1, 2))
   .pipe!(qux(3))
   .pipe!(corge())

This is a common macro in the Lisp world, e.g. Clojure has the -> macro:

; Equivalent to (corge (qux (bar foo 1 2) 3))
(-> foo
  (bar 1 2)
  (qux 3)
  (corge))

Basically, it gives a "method-like" chaining syntax for regular functions, which often makes them a lot more readable.

Since Rust prioritizes methods and traits so much, I'm not sure how useful a macro like that would be, but it's very nice in Lisps, since they tend to use functions a lot more.

@warlord500

This comment has been minimized.

Copy link

warlord500 commented Jun 19, 2018

another, rather interesting use case for this is the try macro in the standard library could be useful for this. ? and postfix try though could start being serious contenders for propagating errors up the chain

@mehcode

This comment has been minimized.

Copy link

mehcode commented Jul 27, 2018

Just thought of a use case that I'd love to see enabled by this. Was browsing RFCs and came across: #543

let fmt = "Hello, {}".to_string();
let s = fmt.format!("Bob");

assert_eq!(&s, "Hello, Bob");
@mikeyhew

This comment has been minimized.

Copy link

mikeyhew commented Sep 16, 2018

Would it be possible to add unwrap! and similar postfix macros to std today, without stabilizing the mechanism for declaring them, so that some day in the future we can do postfix macros the right way (with type-based dispatch) without breaking backward compatibility?

@sagebind

This comment has been minimized.

Copy link

sagebind commented Mar 23, 2019

Here's another example inspired by a Reddit conversation today:

macro_rules! with {
    ($self:self, $( _.$call:ident($($arg:expr),*); )*) => ({
        $(
            $self.$call($($arg)*);
        )*
        $self
    })
}

This is kind of replicates Kotlin's scope functions, which are a pretty cool way of replacing method chaining with something more flexible and readable. It might be used like this:

let config = SomeConfig::default().with! {
    _.foo("bar");
    _.baz(42);
};

The idea is that with! here could work in place of builders and is interesting because it works with both &mut T and T types, sidestepping the age-old question of which Self type builders ought to return. This was just a short example, but I'm sure we could improve the macro to allow you to do more interesting things as well, or maybe improve the inner syntax a bit.

@tikue

This comment has been minimized.

Copy link

tikue commented Mar 23, 2019

Is there anything about with that lends itself to postfix? Even the name suggests prefix to me:

with!(Config::default,
    _.foo("bar");
    _.baz(42);
);

looks fine to me.

@sagebind

This comment has been minimized.

Copy link

sagebind commented Mar 23, 2019

Well the idea is to mutate some value and then return a value, so the first param isn't necessarily a constructor:

let window = self.systems.get_mut<GfxBuilder>().with! {
    _.resolution = (800, 600);
    _.enable_vsync();
    _.build_window()
};

Seems clearer and easier to read than

let window = with!(self.systems.get_mut<GfxBuilder>(), {
    _.resolution = (800, 600);
    _.enable_vsync();
    _.build_window()
});

Though I understand the viewpoint that the improvement is too minor to justify this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.