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

Though it does need to be noted that if you have T and the method/function takes &T or &*T you need to have the closure, in order to get .'s auto(de)ref behavior.

3 Likes

How you would define "simplicity" then?

I don't understand what's the problem. But I want to understand.

For me code with it was always more comfortable that |x|x.something(). It reads intuitively like in human language while more explicit version has additional esoteric meaning. Plus less character noise almost always allows me to be more focused (short term memory capacity).

Every opinion counts!

Some previous discussion on this topic:

2 Likes

Yes, currently, although Iā€™ve seen proposals to make such situations coercion sites. I think that alone would be a great ergonomic boost in exchange for a lot lower complexity (since coercions already exist, so it wouldnā€™t require a new language feature, only the wider application of an existing one).

1 Like

I'm certain that I saw an iterator named it in code I reviewed about five hours ago while chasing a link from irlo or urlo, but I haven't been able to find it again. A search of some of the compiler files turned up

fn test_adapter<T, I: Iterator<Item=Option<T>>>(it: I) {
    is_iterator_of::<Option<T>, _>(&it);  // Sanity check
    let adapter = Adapter { iter: it, found_none: false };
    is_iterator_of::<T, _>(&adapter); // OK
    is_iterator_of::<Option<T>, _>(&adapter); //~ ERROR type mismatch
}

in a test of associated_types_issue_20346

2 Likes

Unless we are code golfing we should not aim to minimize the number of characters. Simplicity is instead a matter of the number of concepts involved. Depending on the context we could also take into account the complexity of those involved complexity themselves are adding in some manner.

Regarding my proposal expression.as identifier.scope:

  • It adds a new operator to the language meaning in the following scope the indicated identifier refers to the result of the previous expression.
  • It employs the already existent keyword as, but in a moderately different way. It is coherent with the natural meaning of the word.

Regarding the proposed expression.bracketed_scope:

  • It adds a new operator and a new kind of scope.
  • There is also a new keyword it.
  • This scope consists of special coma separated expressions. For each of these special expressions, if they contain it they are treated as expressions in which it refers to the result of the expression preceding the operator. If there is no it then the special expression must began with a method of the type of the preceding expression and it is applied to its result.
  • The meaning of the "contain" in the previous item is more complicated than it seems, because the possibility of writing a().b().[c().[f(it)].d(),g(it)].e(). Must c be a method? The expression c().[f(it)].d() apparently contains an it.
  • The natural meaning of it could refer to anything, not just to the preceding expression.

Regarding |x|x.f() vs it.f().

  • The |a|b operator is already in the language while the alternative requires adding a new keyword.
  • The expressions themselves have approximately the same concepts. In one you need to remember what |a|b does and in the other the existence of the special name it.

For me the lambda expression is more natural. This is of course a subjective matter and difficult to satisfy everyone.

This is a reason supporting the feature. Even if some expressions have the same complexity, or conceptual load, one of them can be more intrusive respect to the remainder of the code. In my opinion a little binding is very little invasive.

For example, from

let x=thing.a().b().c().d();

is conceptually very similar to

let x_a=thing.a();
let x_b=x_a.b();
let x_c=x_x_b.c();
let x=x_c.d();

Mostly the latter introduces a few additional names. Nevertheless is clearly more intrusive, since it is eating more lines of the screen.

I think we can state that introducing it introduces complexity in the language and in return we can reduce a little how intrusive are some statements. In contrast introducing the chaining operator (without it) adds some complexity but also allows to reduce greatly the intrusiveness of some common cases. Does chaining plus it reduce the intrusiveness more than just the chaining? And relative to the binding alternatives?

1 Like

Thank you for comprehensive explanation. Now it's clean that we was talked about simplicity of different things: you was talked about simplicity of language itself, while I was talked about simplicity of code written in language.

And right, as language feature my proposal either with and without it is complicated and adds a lot of new concepts that either could be hard to learn and hard to implement.

But I don't think that this complexity is so bad because it's the same as complexity of lifetimes: it either gives us superpower and we wouldn't have too much problems with it when working with code. That said, snippets like a().b().[c().[f(it)].d(),g(it)].e() would never occur in real world and instead they would contain a lot of context alongside that would allow us to understand what's going on.

In code this syntax would only reduce number of concepts. That's not visible on small examples, through. Better do demonstrate a bigger example here and I think I've found the perfect one.

Let consider the following snippet from druid GUI library:

fn main() {
    druid_win_shell::init();

    let mut file_menu = Menu::new();
    file_menu.add_item(COMMAND_EXIT, "E&xit");
    file_menu.add_item(COMMAND_OPEN, "O&pen");
    let mut menubar = Menu::new();
    menubar.add_dropdown(file_menu, "&File");

    let mut run_loop = win_main::RunLoop::new();
    let mut builder = WindowBuilder::new();
    let mut state = UiState::new();
    let foo1 = FooWidget.ui(&mut state);
    let foo1 = Padding::uniform(10.0).ui(foo1, &mut state);
    let foo2 = FooWidget.ui(&mut state);
    let foo2 = Padding::uniform(10.0).ui(foo2, &mut state);
    let button = Button::new("Press me").ui(&mut state);
    let buttonp = Padding::uniform(10.0).ui(button, &mut state);
    let button2 = Button::new("Don't press me").ui(&mut state);
    let button2p = Padding::uniform(10.0).ui(button2, &mut state);
    let root = Row::new().ui(&[foo1, foo2, buttonp, button2p],&mut state);
    state.set_root(root);
    state.add_listener(button, move |_: &mut bool, mut ctx| {
        println!("click");
        ctx.poke(button2, &mut "You clicked it!".to_string());
    });
    state.add_listener(button2, move |_: &mut bool, mut ctx| {
        ctx.poke(button2, &mut "Naughty naughty".to_string());
    });
    state.set_command_listener(|cmd, mut ctx| match cmd {
        COMMAND_EXIT => ctx.close(),
        COMMAND_OPEN => {
            let options = FileDialogOptions::default();
            let result =ctx.file_dialog(FileDialogType::Open, options);
            println!("result = {:?}", result);
        }
        _ => println!("unexpected command {}", cmd),
    });
    builder.set_handler(Box::new(UiMain::new(state)));
    builder.set_title("Hello example");
    builder.set_menu(menubar);
    let window = builder.build().unwrap();
    window.show();
    run_loop.run();
}

And now let compare it with the same rewritten with this syntax:

fn main() {
    druid_win_shell::init();

    let file_menu = Menu::new(). [
        add_item(COMMAND_EXIT, "E&xit"),
        add_item(COMMAND_OPEN, "O&pen"),
    ];
    let menubar = Menu::new(). [
        add_dropdown(file_menu, "&File"),
    ];

    let mut run_loop = win_main::RunLoop::new();
    let mut builder = WindowBuilder::new();
    let mut state = UiState::new();
    let foo1 = FooWidget.ui(&mut state);
    let foo1 = Padding::uniform(10.0).ui(foo1, &mut state);
    let foo2 = FooWidget.ui(&mut state);
    let foo2 = Padding::uniform(10.0).ui(foo2, &mut state);
    let button = Button::new("Press me").ui(&mut state);
    let buttonp = Padding::uniform(10.0).ui(button, &mut state);
    let button2 = Button::new("Don't press me").ui(&mut state);
    let button2p = Padding::uniform(10.0).ui(button2, &mut state);
    let root = Row::new().ui(&[foo1, foo2, buttonp, button2p],&mut state);

    state. [
        set_root(root),
        add_listener(button, move |_: &mut bool, mut ctx| {
            println!("click");
            ctx.poke(button2, &mut "You clicked it!".to_string());
        }),
        add_listener(button2, move |_: &mut bool, mut ctx| {
            ctx.poke(button2, &mut "Naughty naughty".to_string());
        }),
        set_command_listener(|cmd, mut ctx| match cmd {
            COMMAND_EXIT => ctx.close(),
            COMMAND_OPEN => {
                let options = FileDialogOptions::default();
                let result =ctx.file_dialog(FileDialogType::Open,options);
                println!("result = {:?}", result);
            }
            _ => println!("unexpected command {}", cmd),
        }),
    ];

    let window = builder. [
        set_handler(Box::new(UiMain::new(state))),
        set_title("Hello example"),
        set_menu(menubar),
        build().unwrap()
    ];

    window.show();
    run_loop.run();
}

In different parts of this code we don't have:

  • Unnecessary mut bindings and annotations
  • Builder patterns (many chained using this syntax methods don't returns self)
  • Explicit referring to defined previously bindings

In sense of simplicity that example is very similar to your that removes unnecessary bindings and in return we have more simpler abstraction.

On my example in return we have:

  • Clean definition of immutable items: file_menu, menubar, foo1, button, etc
  • Clean definition and usage of mutable parts: run_loop, builder, state
  • Clean separation of sequences of actions from imperative (possibly interfering) scopes

And as a side note: this example also explains why explicit receiver is redundant:

  • The meaning of code almost always is obvious on local context
  • Explicit receiver would name things twice (e.g. state would be also referred as it)
  • Explicit receiver would look the same in different contexts which would make code more obscure

Most likely it wouldn't be used very often, so either it don't adds too much complexity to code and either we can go without it. However, IMO when having ability to chain everything except external functions, the extended dot feature would look bizarrely incomplete.

Consider the same example where we modify file_menu in other function without it:

fn main() {
    druid_win_shell::init();

    let mut file_menu = Menu::new(). [
        add_item(COMMAND_EXIT, "E&xit"),
        add_item(COMMAND_OPEN, "O&pen"),
    ];
    modify_menu(&mut file_menu);
    let menubar = Menu::new(). [
        add_dropdown(file_menu, "&File"),
    ];

    let mut run_loop = win_main::RunLoop::new();
    let mut builder = WindowBuilder::new();
    let mut state = UiState::new();
    let foo1 = FooWidget.ui(&mut state);
    let foo1 = Padding::uniform(10.0).ui(foo1, &mut state);
    let foo2 = FooWidget.ui(&mut state);
    let foo2 = Padding::uniform(10.0).ui(foo2, &mut state);
    let button = Button::new("Press me").ui(&mut state);
    let buttonp = Padding::uniform(10.0).ui(button, &mut state);
    let button2 = Button::new("Don't press me").ui(&mut state);
    let button2p = Padding::uniform(10.0).ui(button2, &mut state);
    let root = Row::new().ui(&[foo1, foo2, buttonp, button2p],&mut state);

    state. [
        set_root(root),
        add_listener(button, move |_: &mut bool, mut ctx| {
            println!("click");
            ctx.poke(button2, &mut "You clicked it!".to_string());
        }),
        add_listener(button2, move |_: &mut bool, mut ctx| {
            ctx.poke(button2, &mut "Naughty naughty".to_string());
        }),
        set_command_listener(|cmd, mut ctx| match cmd {
            COMMAND_EXIT => ctx.close(),
            COMMAND_OPEN => {
                let options = FileDialogOptions::default();
                let result =ctx.file_dialog(FileDialogType::Open,options);
                println!("result = {:?}", result);
            }
            _ => println!("unexpected command {}", cmd),
        }),
    ];

    let window = builder. [
        set_handler(Box::new(UiMain::new(state))),
        set_title("Hello example"),
        set_menu(menubar),
        build().unwrap()
    ];

    window.show();
    run_loop.run();
}

Here, IMO all clarity of the whole main function is gone. You can't be sure that file_menu isn't modified somewhere below. When you see some &mut you on a moment think that file_menu may be on the right side. There's no more any clean separation of mutable state from immutable. And action that is run in sequence now is moved outside of brackets.

Thus, by keeping simplicity in one place we sacrificing in simplicity in another.

In such cases it would be a saviour, even if by itself it's not very clean. So, IMO increased language complexity here is an acceptable price to pay for it.

Thereā€™s a reason to Rust requires self. rather than searching for methods in the definition of methods; local clarity of semantics.

Given that there isnā€™t a concept of receiver types currently in Rust, I donā€™t feel that introducing a construct that provides a receiver, though locally obvious, is desirable.

tapping is indeed a useful construct for initialize-mutable, use-immutable patterns. But the example youā€™ve picked works great with a closure and doesnā€™t have any reason to be a first class construct.

(Also, making suboptimal library design easier to use isnā€™t exactly a great motivator either; most builders do chain returning &mut self to allow the ā€œfluent builderā€ style.)

4 Likes

I think that dot in proposes syntax introduces this sort of clarity and there's no need in something bigger.

I don't see any relation, through. The fact that there isn't a concept of receiver types don't implies that there can't be something like that. Feature can be unique.

I'd be rather surprised to see it somewhere in code. I even don't think that it would be harder to understand how proposed syntax works than how this crate works. It introduces a lot more concepts in code.

But is there a reason for it to be a closure? I don't see any. Closures are imperative, verbose and have problems with control flow propagating. I don't see any reason for it to not be a first class construct

If you are worried about file_menu not being changed after you make it, then initialize it inside a block,

let file_menu = {
    let mut file_menu = ...;
    // ... your init code here ...
    file_menu
};

Now it is perfectly clear that file_menu is being initialized here, and that it will not be changed for the duration of the function after initialization. Also, if you really wanted the method syntax, you could just implement them using some private traits, which would give you the actual method syntax, at a minor cost of making and implemented the trait, which could be eased with a macro. This proposal also seems rather trivial to solve using a macro. Because of these three points, I don't think you have sufficient motivation to change the language.

A minor thing, I don't like using [ ] to delimit scopes, it seems weird.

Iterators have a method called inspect. tap generalizes this to all types, with some helpful functions for Option, Result, and Future.

If there is a perfectly good way to do it without first class constructs, then we don't need the first class constructs. First class constructs are more expensive in the complexity budget, so it is good to avoid adding too many of them. Because I think you have insufficient motivation, and you are dramatically increasing the complexity budget, I don't think this is a good proposal.

3 Likes

It's clear only after a bit of examination. There's a lot of boilerplate and there's that intrusive mut which I don't want to see at all. I've tried this pattern and IMO it's slightly better than original. But it don't solves all other problems.

I don't need yet another DSL. Reading of custom macro definitions and trait extensions for things that are basic almost always makes me sick.

It don't provides a full featured scope. It provides something like a list of actions that would be applied to receiver value. I don't like it too, but there can't be a better syntax.

There's no perfectly good way. There's just a way to do it.

I agree that this feature increases the complexity budged, but definitely not dramatically. And aren't first-class chaining of await, pipeline operator, method cascading, and postfix macros a good motivation? What would be a good motivation then?

Exactly ā€“ if something can be done reasonably using existing features, then it shouldn't be a new feature. (Where "slightly annoying and "not immediately obvious to everyone" don't contradict "reasonable".)

Right, but I think there is something useful here and that we can get it with just a minor extension. If we for example extend the meaning of the dot operator e.f to functions f having a single argument as to apply the function to the preceding expression e, then things seems to be rather simple and compact. This is very similar to the proposal above by @eaglgenes101. Example:

    let x = a().b().|t|{
        f(t);
        g(t)
    }.c().|t|{
        h(t);
        i(t)
    }.d();

How would we code this currently? One option is

let x={
   let mut t=a().b();
   f(t);
   let mut t2=g(t).c();
   h(t2);
   i(t2).d()
};

To me, this extension seems minor: there is not a new kind of scope neither new keywords. The only thing that feels strange is to restrict the function to a single argument. Also, the other described examples seem to be able to be rewritten as this.

If rust needs postfix operators, then I would prefer the following syntax

  • foo().?await is short hand syntax for await!(foo())
  • foo().?try is a possible alternative syntax for foo()? (Added for consistency)

I think this solution is somewhat constent with existing syntax.

You could implement this entire proposal as a macro, then why do we need to extend the language to do this. This sort of proposal is exactly why we have macros in the first place.

3 Likes

For example, to simplify the boiler plate of making and maintaining a trait to extend a type to get the dot syntax, you can use a macro similar to this.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=85e450605d11ec1de4c880adbe02ae0a

Note: I know this is not completely general, it is meant to be a proof of concept. (for example, generic functions are not supported, but that should be easy enough to extend).

Note 2: The syntax that the macro uses is very similar to the existing syntax of impl blocks, and could be exactly the same if you invest some time to make a proc macro.


Like this, you could make a macro that forwards the receiver to a set of functions automatically, then you could use that instead of changing the language.

3 Likes

Because macros are verbose, increases complexity of code, and can't provide a sufficient solution?

At least postfix macros would be needed to implement something like that. I just don't see any sense to use implementation with current macros instead of plain Rust.

That complexity has to go somewhere, be it in your code or in the compiler. I would like to keep unnecessary complexity out of the compiler.


Again, this is a proof of concept, but I implemented a subset of your syntax with macros alone.

This isn't that verbose. Combined with my previous macro, you could implement your entire proposal with macros, and maybe some minor changes to syntax.

3 Likes

Then why constructs like if, for, ?, try, etc arenā€™t implemented as macros and not moved into separate libraries? They definitely could be. They are also unnecessary in presence of match/loop/break/return

They are very well known and tested constructs, everyone needs them in nearly all the code.

And besides, ? did start as a macro, to test it out. It worked well, so it got promoted to a full operator.

5 Likes