A final proposal for await syntax

#203

I’m not sure what more can be done to demonstrate careful consideration. An RFC at this point seems like purely a formality, and to me does nothing to signal careful consideration.

There are many threads about the syntax, and the lang team has been reading them. That’s pretty impressive considering the sheer volume involved and the low signal to noise ratio - it has mostly been rehashing the same arguments. The way this has been conducted screams “carefully considered” to me. Multiple blog posts, large decision timeframes, and being (what I see as) transparent.

12 Likes
#204

People don’t read the entire language model up front before using a language for the same reason that we teach kids Newtonian physics first instead of just handing them a quantum mechanics textbook. Or the same reason that we don’t teach programming by first handing them the spec sheet for the x86 processor. People have to bootstrap their knowledge by building small mental models and building on them iteratively and developing an intuition around each successive piece of the puzzle. That’s why we have hello world projects. We need to have a learning feedback loop. There’s a fair amount of psychological research on the matter, but it more or less comes down to a loop of developing a internal model of the world, making a prediction on how the world will respond to some input, and then seeing how accurate the prediction was and adjusting the mental model accordingly.

That’s why anything that will knowingly add a temporary breakage in the mental model that had developed up to that point represents a jarring user experience. I think that <dot><keyword> having side effects as in .await, while technically consistent in the domain of the language, is very likely to break any mental model that a programmer had developed over the course of them learning the language.

The Design of Everyday Things deals with this usability problem with regard to tools and machines. A programming language is really just a tool to build things, so I’m trying to make sure we consider the usability of the tool and make sure the tool is intuitive for people who are going to use it. If we can avoid them having to read an error message (even if they are able to read it once and never make the same mistake again), then we should do so.

5 Likes
#206

Semantic arguments about how much documentation people should be expected to read are off-topic for this discussion about await, and dismissing an entire field of study is off-topic and inappropriate for the entire forum. Please try to bring it back on track.

20 Likes
#207

Hi, newcomer and non-expert here, but I will like to share my 2cents. I have come to prefer the dot operator as the sigil for postfix await syntax. What comes after the dot, I’m not too sure since I don’t know enough yet, but “dot await keyword” seems like a reasonable choice. What follows are my personal musings, but I’m hoping it will help “connect the dots” around some of the topics I saw on the dot operator.

At first, I liked the idea of “universal pipelining”. But as I read more on it, I noticed several mentions of how the dot operator already or can serve this purpose in Rust (1, 2, and most interestingly 3). This helped me gain a more nuanced understanding of the dot operator.

In fact, when I take a more holistic view, I realize that the dot operator is already doing some magical things and not just plain old field access. I like how @jcsoo described it, and @scottmcm’s notion of “namespaces”. My interpretation is that . let’s you do:

  • struct field access, when given a named field identifier;

  • universal function call, or typically “method call”, but with the nuance that the method is not a member of the struct but rather of the type and so there’s magic to perform UFCS.

But it occurred to me that there’s a third thing, and that is tuple indexing. I haven’t seen tuple indexing mentioned in the various threads yet, so I think this is “new information”. Essentially, when I read of people mentioning “conflict with field access”, I subconsciously think of struct field access. But when I put tuple indexing into the picture, things start to click and that dot-await no longer seemed weird but rather just fits.

Consider foo.12 for a minute. 12 could not be a member of foo, since digits alone are not allowed as identifiers. So rather than field access, this is doing some magic under-the-hood for indexing into the tuple. When I was first learning tuples in Rust, my first reaction to this was “What on earth?!!”. But I learned it, used it, and it quickly became “This is so cool!”.

For me, in the case of foo.await, I see await is a keyword and so it’s not a field identifier, thus prompting me to think of “magic” and relates it to tuple indexing. Obviously, futures are not tuples and awaiting is a totally different beast than indexing. I don’t know much about the implementation, the macros, executors, polling, etc, but if we’re just talking about the syntax then my take is that: dot-await will be weird like how tuple indexing is weird, but it might just become “normal” like how tuple indexing is normal nowadays. If we go with the notion of namespaces, then tuple indexing introduced a new namespace of indices, while dot-await will introduce a new namespace of keywords, and these are unified under the dot operator.

As an aside, something else that I find amusing, is that the other operator that deals with namespaces is ::, which is a bunch of dots.

I’m sure some things in my thinking might be flawed or shallow, but that’s the conclusion I had drawn for myself, and others may draw a different conclusion. I will like to thank the lang-team for working through this and thank the community for sharing! I have learned a ton reading through all these discussions.

16 Likes
#208

Some people have a misunderstanding that the dot operator is pure, while this is encouraged it is not true. @yufengwng pointed out, the dot operator does some magic, but this time with Deref.

While this example is contrived and is an extreme anti-pattern, it does prove the point that we can’t really trust the dot, unless we know something more about the types.

struct Foo;
struct Bar;

impl Foo {
    fn do_work(&self) { unreachable!() }
}

impl Deref for Bar {
    type Target = Foo;
    
    fn deref(&self) -> &Foo {
        loop { do_some_blocking_io(); }
    }
}

// .. later ..

Bar.do_work();
unreachable!();
5 Likes
#209

Here’s an even better example:

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

use std::ops::Deref;

struct Foo {
    value: i32,
}

struct Bar {
    foo: Foo,
}

impl Deref for Bar {
    type Target = Foo;
    
    fn deref(&self) -> &Self::Target {
        println!("Firing the nukes!");
        &self.foo
    }
}

fn main() {
    let bar = Bar {
        foo: Foo {
            value: 10,
        },
    };

    // Not a method call, but it still has side effects!
    bar.value;
}
20 Likes
On why await shouldn't be a method
On why await shouldn't be a method
#210

Oh, that’s amazing! I forgot about fields.

#211

Given this it’s probably even possible to cobble together an extremely cursed variation of computed fields by dereffing to a type containing just the field as a Cell (to get & mutability).

This argument really just goes to show that the dot does indeed run user code already. And dtonlay has hacked together read-only fields using deref and proc macro madness. Dot is already more powerful than you realize.

3 Likes
#212

But these all work because we don’t move from the target. For example this doesn’t because the auto-deref will lead to trying to call the impl on a moved value, but we only have a reference to that:

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

struct NoCopy;

struct A {
    foo: NoCopy,
}

fn main() {
    let a = A { foo: NoCopy, };
    a.f_await();
}

impl std::ops::Deref for A {
    type Target = NoCopy;
    fn deref(&self) -> &NoCopy {
        &self.foo
    }
}

impl std::ops::DerefMut for A {
    fn deref_mut(&mut self) -> &mut NoCopy {
        &mut self.foo
    }
}

trait Await {
    fn f_await(self);
}

impl Await for &'_ mut NoCopy {
    fn f_await(self) { }
}

impl Await for NoCopy {
    fn f_await(self) { }
}

Despite how fun your example is in terms of pushing the boundaries of what is expressible with current syntax (and so is the linked crate from @CAD97 ), I don’t know if its serves as an argument for using dot in new special ways if it is even surprising to more experienced Rust programmers? For me, that would be a reason to avoid introducing additional complexity into the . operator and so to make .await work at least consistent with current other .-operations. Could be fine though.

Using deref seems to break down when we are interested in by-value semantics my above example. If applied consistently, then .await would work syntactially and semantically on values dereferencing to a &'_ mut impl Future but nevertheless fail to compile due to trying to move from the referenced value. Which is kind of weird and maybe unfortunate but at least consistent and evokes a more familiar error message from the compiler. Furthermore, the by_ref solution¹ would then apply to both Iterators and Await for the same reasons instead of different ones.

¹ compare with iterator

trait Future {
    fn by_ref(&mut self) -> &mut Self { self }
}
1 Like
#213

Actually, there are proposals for implementing a DerefMove (and IndexMove) traits:

So if anything, it seems like . will probably have even more magic behavior in the future (regardless of what happens with await)

The simple fact is that . has always been magic in Rust (and for good reason), this isn’t unusual.

And other languages like JavaScript have getters/setters which allow you to run arbitrary code when a property is accessed or set:

class Foo {
    get bar() {
        console.log("Getting bar");
        return 5;
    }

    set bar(value) {
        console.log("Setting bar to", value);
    }
}

let x = new Foo();
x.bar;
x.bar = 10;

Many other (popular) languages can also do this. They even have a principle for it. It isn’t unusual at all for fields/properties to have magic behavior, or to run user code.

4 Likes
On why await shouldn't be a method
#214

But deref can move for (the special case of) Box, and there’s intent to make it no longer a special case so Box can be just another library type, rather than a completely new kind of type like it is today.

You can make the argument that dot is already too magical because of all this. I say it means dot has more power than you’re insinuating when you say .await doesn’t fit with field†‡ access.

† and method
‡ through deref

2 Likes
#215

When I initially brought it up it was to show that any argument based on the idea that adding future.await would make the dot do something magical (like blocking) that it wasn’t able to do before were unfounded. Nothing more.

Also, how often would you actually have these exotic futures (smart pointers wrapping futures)? I can’t think of many reasons for storing futures in user-land code other than boxing, behind a reference or in a Vec like structure. (Although that may just be due to lack of imagination).

2 Likes
#216

Uh … It’s a matter of word choose I believe. For example:

You know await thing from JavaScript? The syntax in Rust is thing.await rather than await thing

My nature respond will be “Why?”

By the way, I think people already in the rabbit hole discussing whether or not .await is better. Well, actually, people are talking about whether or not .await, @await, await!, await x etc is better. Which is why you have all this chaos.

1 Like
#217

The simple answer is: because it works really well with ?

Other languages like JavaScript and C# use exceptions, not ?, so that’s why they’re different.

1 Like
#218

Thanks for the pointer on these language semantics and proposal. That’s a bit of consistency I was missing quite some time. Though the deref for Box of course only applies to Box by value, which currently has an impl for Future as well, so that’s fine.

Sure, that is not the part which is amiss for treating .await as some field. Rather, it is the part where we can only get it by value, and not reference, which makes its syntactic equivalence to a field a bit unusual. The ops::Deref tricks all achieve the opposite of that. And awkward to integrate into current auto-deref semantics, so I see the argument for not integrating it and keeping it seperate. ‘Works similar to (what is hidden behind something for) a field’ is still quite an overstatement, I think. Hence, I still favor .await() which doesn’t suggest that.

If I have some sort of shared future (imagine Cell equivalent, as in two non-mutable reference awaiting the same underlying one etc.) then I could want to await an Arc. Or the possibility to await one in a MutexLock as discussed, … One indicator of good design, in my eyes, is the number of possibilities it enables outside what is immediately specified precisely because it is hard to envision all permutations and combinations in the initial definition.

#219

Wouldn’t you just immediately await futures for the most part? No need for it to even be stored. That said, I do agree that good design will enable possibilities outside of the original definition.

Just a nit: Cell doesn’t really work with Deref, you can only really move into or out of a Cell, not take a reference to the insides (unless you have a &mut Cell<_>)


Also another reason why await field won’t be that confusing is because most futures are anonomous either by async or impl Future so you couldn’t access fields anyways, this should lead people to realize that something special is going on.

How will Promise.all be implemented?
#220

That’s not what my main point was. What I’m pointing out, is that

The barrier here is very low, as in "You know await thing from JavaScript? The syntax in Rust is thing.await ". Done. People are very capable of doing this transfer.

Is not how you prevent people from jump into rabbit hole.

By the way again, you already in the rabbit hole. I can continue with your reply like:

Why it works well with ? Why wouldn’t (await thing)? (or insert any other syntax here) work?

If you lookup the discussions, you will found this pattern. Continue this pattern will not resolve the problem.

How about do a summary to figure out what people is worrying, and then try to come out with another plan that won’t cause such worries?

1 Like
#221

I thought we were discussing about how to teach this syntax to people coming from other languages (or beginners)? Not about how to solve the current controversy?

#222

Sure, that’s why I chose the other wording. :slightly_smiling_face:

I also feel like you quoted the wrong sentence, but I’m not sure. The one you quote is the negative example, based on the suggestion to have both syntaxes.

1 Like
#224

perfect use of this meme btw, :trophy:

2 Likes