A final proposal for await syntax

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

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

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

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

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:

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

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

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

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

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

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

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.

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.

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

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?

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

perfect use of this meme btw, :trophy:

2 Likes

There was a lot of discussion about deref above. But I didn’t follow how it would relate to .await.

If I have a Box<Future<_>> can I do a .await on the box?

If not, then it’s odd because that means that the dot is not invoking deref. (Would it be ..await? And would that cause problems with Range?)

If I can, it seems odd because I could write a struct that implements Future and can be derefed into something generic and that is also a future? How would I disambiguate those calls and force one or the other?

1 Like

Some people have brought up the argument that they don't like .await because it looks like field access.

According to them, field access is pure and has no side effects, and is a very cheap operation, unlike .await

However, Deref proves that field access is not pure, not cheap, and it can have side effects.

Therefore, reusing the field syntax for .await isn't any worse than Deref

You can, but not because of Deref. There is an implementation of Future for Box.

As far as I know, .await will not trigger Deref behavior. Which is actually a very interesting inconsistency, that I haven't seen anybody bring up!

1 Like

To me "dot doesn't deref" would be far more surprising "the keyword is on the right".

6 Likes

@Pauan, @tkaitchuck: see .await and auto-deref for the parallel discussion on deref.

2 Likes