[Pre-RFC] Extended dot operator (as possible syntax for `await` chaining)

That's not true, all of them are different than we known, and all of them can be substituted

If I remember correctly, ? was started as ergonomic improvement over try!(), and try!() was started as macro without any plan to transform it to ? (that may be incorrect, through).

This is not a valid argument to add more unnecessary cruft to the compiler.

Implementing it as a macro first doesnt prevent it from gaining native support. However, it does let people play with the new syntax / feature.

Iā€™d saying implenting something as a macro increases the chances that it will gain native support. Like the try/? thing.

OTOH, it might also reduce the chance of inclusion if it is something thatā€™d be cause for regret - it makes it easier to see upsides and downsides. I think generally a macro implementation (vs no implementation) gives the community more information to work with when deciding to include / exclude a feature.

So hereā€™s my vote for making a macro implementation that can be played around with.

4 Likes

More to the point, if you insist on waiting for language and compiler changes, youā€™re going to wait for a long time, with progress gated by two overworked teams that already have large backlogs. If you experiment now with a macro implementation, you can proceed at your own pace. Why would you not implement this now as a macro, so that you and others can use it now?

5 Likes

I think this feature should be possible to implement with proc macros (even including the it magical variable).

Basically, everything inside of the dot! can use whatever custom syntax you want:

dot! {
    let result = client.get("url").[await send()]?.[await json()]?;

    result.[foo()].[bar(it)]
}

It might be possible to use syn to parse the code (not sure how it will handle the postfix [] though).

I donā€™t really understand the argument that macros are somehow gross or complex or bad: sure they can be complex to create, but theyā€™re not complex to use.

Of course you have to go and read the documentation for the macro in order to know how to use itā€¦ but thatā€™s also true with functions and ordinary methods!

And learning how to use a macro (such as the above hypothetical dot! macro) isnā€™t any harder than learning new language syntax. Either way you have to learn how to use it.

I agree that this feature is worthwhile, but I think it should be created as a macro first, for experimentation.

3 Likes

But that's a valid argument that implementing every language feature as macro first is unnecessary.

We already have nightly channel for testing new syntax / features. In this way users would have first class syntax, and there wouldn't be any migration headache. I also would be more ergonomic, so in this form it could attract more users for testing.

I can wait as long as it will be required - that's not a problem. I know that the language team is currently busy and that this feature couldn't be in priority in near future. It could be rejected at all - that's fine.

Everything I want currently - to represent this idea as best as possible.

Probably the most important reason that I don't like macro implementation is that I don't have too much experience with macros and currently I don't know how to implement exactly the same syntax using them:

  • how to inject it keyword into expression and restrict it scope to single level?
  • how to return last value depending on presence of comma?

It don't looks trivial, and I don't know what pitfalls here could be. It would require a lot of investigation.

Also, I trying to avoid custom macros when possible and I wouldn't use it in any real world code. For testing purposes it's always enough for me to rewrite some snippet with desired syntax. I fear to promote a macro solution to community because it easily can create wrong impression, especially if there wouldn't be any way to provide the same guarantees as in original proposal.

Also, it would be honest to say that I'm not motivated enough for it.

It don't looks that bad. But it looks more complex than all other macros included in Rust. If someone will implement it as separate crate preserving all properties of this construct - that's ok. However, I don't think that it would be suitable as final solution that should be included into Rust.

Code with macros is harder to understand. Macros aren't beginner friendly. They have problems with error reporting. They have problems with IDE.

IMO, this macro might will introduce more problems than solve.

But we don't implement every language feature as a macro, I can only think of 1 that has, ?. This proposal is simple enough that it can be implemented as a macro, so it should. That way as others have stated, we can see if we need to go further and implement it in the compiler, or if it was a bad decision and deprecate it. As long as it is a macro, we retain some flexibility.

Constructs like if, for, try aren't implemented with macros because with if and for, there is a long history for how they should work, and they lean make it easier for someone with a background in C or C++ to convert to Rust. This was one of Rust's founding principles. try couldn't be implemented as a macro while preserving some important of the features required for them to work well. You could approximate it with this macro:

macro_rules! try_block {
    ($($block:tt)*) => {
        (
        || {
            $($block)*
        }
        )()
    }
}

But you can't you break or return inside a try block, like you would in the actual try block.

2 Likes

For if someone continues to be interested in this feature, I have just implemented a macro for the explicit case using functions (instead of the it variable) and using the => symbol. I have used this symbol because it seems that expr can only be followed by a few symbols. It is the first macro I have implemented so I may have missed something.

macro_rules! chain_expand{
    ( $first:expr, $($a:expr,)* @ $($z:expr,)*) => {
        chain_expand!( $($a,)* @ $first, $($z,)* )
    };
    ( @ $first:expr, $($z:expr,)+ ) => {
        $first(chain_expand!( @ $($z,)+ ))
    };
    ( @ $unique:expr,) => {
        $unique
    };
}

macro_rules! chain{
    ( $($a:expr)=>*) => {
        chain_expand!($($a,)* @)
    }
}

fn main() {
    let x=7;
    let y=chain!(x => |t|{t+t+3});
    dbg!(y);
    let z=chain!(x => |t|{t+t+3} => |t|{t*5});
    dbg!(z);
}

Also related, I have found the RFC https://github.com/rust-lang/rfcs/issues/1579 about infix notation, which seems very related.

Anyway, would be it implemented as macro or not - thatā€™s implementation details. If macro wraps the whole parent expression then IMO itā€™s not too different from plain syntax and discussing it donā€™t makes too much sense, at least at this point. Currently, I just want to finish design of ā€œpureā€ functionality and share it with community.

Also, I have thought that RFC should contain ā€œImplementationā€ section, which would allow to define implementation and functionality details separately, and this would make discussions more focused


Meanwhile Iā€™ve done major rewrite of this RFC, where:

  • Iā€™ve tried to explain things as best as I can
  • it was renamed to this
  • Added ā€œtuple reorganizationā€ use case
  • Included new examples
  • Described a way how to begin action with external function/binding
  • Briefly mentioned alternatives that was provided ITT
  • Removed await syntax usages as itā€™s design isnā€™t decided yet

FYI, Iā€™ve started implementing the RFC as a macro in the extdot crate.

Itā€™s still a work in progress and missing some things from the RFC and doesnā€™t incorporate any of the changes you maid yesterday. Also it allows for a few things for testing reasons like allowing .() or .{} in addition to .[].

The code is also a lot messier than I like but this is the first time Iā€™ve wrote a macro that worked with TokenStream directly since I didnā€™t find the syn crate very useful when working with new syntax.

3 Likes

I did have a few questions that came to mind when implementing the macros:

Question 1

a.b.[c]       ā‡’ { let mut _this = a.b ;{ _this.c   }}

When is this useful? I canā€™t think of a single instance where this is useful, so for now I implemented the following in the macros:

a.b.[c]       ā‡’ { let mut _this = a.b ;{ c(_this)  }}

Question 2

From RFC:

  • here and below _this should be substituted with more unique token

What is the reason to do a substitution at all? I currently have the macros replacing this with this. In other words Iā€™m just using the same binding but replacing the Span to Span::call_site() so it can resolve with rustā€™s macro hygiene.

Question 3

From RFC

  1. Implicit and explicit receiver becomes unavailable inside of nested brace

This seems to me like itā€™s trying to enforce subjective style in the parser itself instead of something like clippy or rustfmt.

1 Like

I think that it's better for consistency: you can access methods and field with regular dot and extended dot should support it as well (looks like we also should allow tuple indexing to make it complete). In current way it's also simpler because in order to understand how it works we just need to imagine that reciver "concatenates" with action.

I don't think that resolving a.b.[c] to binding or function makes more sense. Binding would be useless, would break consistency, and we would introduce another way of doing things. Resolving it to binding as you suggested would be ergonomic, but I think it's still problematic:

  1. Breaks consistency
  2. Don't looks as method calling at all
  3. Don't uses explicit this when we call external function
  4. Looks very close to array indexing (it's not a problem in current RFC because warning would be issued on such snippets)

When implementing extended dot as macro probably it really don't makes sense. But there's reason when implementing it as language construct. I admit that currently this reason isn't obvious at all and the next update to this RFC should clarify it.

IMO extended dot syntax should remain declarative as the whole method call chain is declarative. Bindings are more appropriate for imperative programming. And I currently even don't see any use case where using this inside of braces would make any sense.

New updates:

  1. To make it simpler, embedding language constructs into chain now requires explicit this, e.g. vec.[!this.is_empty()]
  2. All dot properties (method, field, tuple indexing) should be supported by implicit this
  3. Rewritten desugaring section; now it could be seen how replacing of this with unique identifier invalidates this in nested braces
  4. Changed other descriptions

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.