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

Tracking issue for async/await (RFC 2394) #50547

Open
withoutboats opened this Issue May 8, 2018 · 285 comments

Comments

Projects
None yet
@withoutboats
Copy link
Contributor

withoutboats commented May 8, 2018

This is the tracking issue for RFC 2394 (rust-lang/rfcs#2394), which adds async and await syntax to the language.

I will be spearheading the implementation work of this RFC, but would appreciate mentorship as I have relatively little experience working in rustc.

TODO:

  • Implement
  • Stabilize
  • Document

Unresolved questions:

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 8, 2018

The discussion here seems to have died down, so linking it here as part of the await syntax question: https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/7344

@withoutboats

This comment has been minimized.

Copy link
Contributor Author

withoutboats commented May 9, 2018

Implementation is blocked on #50307.

@berkus berkus referenced this issue May 10, 2018

Open

Futures v0.2 #98

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 10, 2018

About syntax: I'd really like to have await as simple keyword. For example, let's look on a concern from the blog:

We aren’t exactly certain what syntax we want for the await keyword. If something is a future of a Result - as any IO future likely to be - you want to be able to await it and then apply the ? operator to it. But the order of precedence to enable this might seem surprising - await io_future? would await first and ? second, despite ? being lexically more tightly bound than await.

I agree here, but braces are evil. I think it's easier to remember that ? has lower precedence than await and end with it:

let foo = await future?

It's easier to read, it's easier to refactor. I do believe it's the better approach.

let foo = await!(future)?

Allows to better understand an order in which operations are executed, but imo it's less readable.

I do believe that once you get that await foo? executes await first then you have no problems with it. It's probably lexically more tied, but await is on the left side and ? is on the right one. So it's still logical enough to await first and handle Result after it.


If any disagreement exist, please express them so we can discuss. I don't understanda what's silent downvote stands for. We all wish good to the Rust.

@alexreg

This comment has been minimized.

Copy link
Contributor

alexreg commented May 10, 2018

I have mixed views on await being a keyword, @Pzixel. While it certainly has an aesthetic appeal, and is perhaps more consistent, given async is a keyword, "keyword bloat" in any language is a real concern. That said, does having async without await even make any sense, feature wise? If it does, perhaps we can leave it as is. If not, I'd lean towards making await a keyword.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented May 10, 2018

I think it's easier to remember that ? has lower precedence than await and end with it

It might be possible to learn that and internalise it, but there's a strong intuition that things that are touching are more tightly bound than things that are separated by whitespace, so I think it would always read wrong on first glance in practice.

It also doesn't help in all cases, e.g. a function that returns a Result<impl Future, _>:

let foo = await (foo()?)?;
@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 10, 2018

The concern here is not simply "can you understand the precedence of a single await+?," but also "what does it look like to chain several awaits." So even if we just picked a precedence, we would still have the problem of await (await (await first()?).second()?).third()?.

A summary of the options for await syntax, some from the RFC and the rest from the RFC thread:

  • Require delimiters of some kind: await { future }? or await(future)? (this is noisy).
  • Simply pick a precedence, so that await future? or (await future)? does what is expected (both of these feel surprising).
  • Combine the two operators into something like await? future (this is unusual).
  • Make await postfix somehow, as in future await? or future.await? (this is unprecedented).
  • Use a new sigil like ? did, as in future@? (this is "line noise").
  • Use no syntax at all, making await implicit (this makes suspension points harder to see). For this to work, the act of constructing a future must also be made explicit. This is the subject of the internals thread I linked above.

That said, does having async without await even make any sense, feature wise?

@alexreg It does. Kotlin works this way, for example. This is the "implicit await" option.

@alexreg

This comment has been minimized.

Copy link
Contributor

alexreg commented May 10, 2018

@rpjohnst Interesting. Well, I'm generally for leaving async and await as explicit features of the language, since I think that's more in the spirit of Rust, but then I'm no expert on asynchronous programming...

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 10, 2018

@alexreg async/await is really nice feature, as I work with it on day-to-day basis in C# (which is my primary language). @rpjohnst classified all possibilities very well. I prefer the second option, I agree on others considerations (noisy/unusual/...). I have been working with async/await code for last 5 years or something, it's really important to have such a flag keywords.

@rpjohnst

So even if we just picked a precedence, we would still have the problem of await (await (await first()?).second()?).third()?.

In my practice you never write two await's in one line. In very rare cases when you need it you simply rewrite it as then and don't use await at all. You can see yourself that it's much harder to read than

let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;

So I think it's ok if language discourages to write code in such manner in order to make the primary case simpler and better.

hero away future await? looks interesting although unfamiliar, but I don't see any logical counterarguments against that.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 10, 2018

In my practice you never write two await's in one line.

But is this because it's a bad idea regardless of the syntax, or just because the existing await syntax of C# makes it ugly? People made similar arguments around try!() (the precursor to ?).

The postfix and implicit versions are far less ugly:

first().await?.second().await?.third().await?
first()?.second()?.third()?
@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 10, 2018

But is this because it's a bad idea regardless of the syntax, or just because the existing await syntax of C# makes it ugly?

I think it's a bad idea regardless of the syntax because having one line per async operation is already complex enough to understand and hard to debug. Having them chained in a single statement seems to be even worse.

For example let's take a look on real code (I have taken one piece from my project):

[Fact]
public async Task Should_UpdateTrackableStatus()
{
	var web3 = TestHelper.GetWeb3();
	var factory = await SeasonFactory.DeployAsync(web3);
	var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
	var request = await season.GetOrCreateRequestAsync("123");

	var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
	var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");

	await request.UpdateStatusAsync(trackableStatus);
	await request.UpdateStatusAsync(nonTrackableStatus);

	var statuses = await request.GetStatusesAsync();

	Assert.Single(statuses);
	Assert.Equal(trackableStatus, statuses.Single());
}

It shows that in practice it doesn't worth to chain awaits even if syntax allows it, because it would become completely unreadable await just makes oneliner even harder to write and read, but I do believe it's not the only reason why it's bad.

The postfix and implicit versions are far less ugly

Possibility to distinguish task start and task await is really important. For example, I often write code like that (again, a snippet from the project):

public async Task<StatusUpdate[]> GetStatusesAsync()
{
	int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
	var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
	var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
	{
		var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
		return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
	});

	return await Task.WhenAll(tasks);
}

Here we are creating N async requests and then awaiting them. We don't await on each loop iteration, but firstly we create array of async requests and then await them all at once.

I don't know Kotlin, so maybe they resolve this somehow. But I don't see how you can express it if "running" and "awaiting" the task is the same.


So I think that implicit version is a no-way in even much more implicit languages like C#.
In Rust with its rules that doesn't even allow you to implicitly convert u8 to i32 it would be much more confusing.

@alexreg

This comment has been minimized.

Copy link
Contributor

alexreg commented May 10, 2018

@Pzixel Yeah, the second option sounds like one of the more preferable ones. I've used async/await in C# too, but not very much, since I haven't programmed principally in C# for some years now. As for precedence, await (future?) is more natural to me.

@rpjohnst I kind of like the idea of a postfix operator, but I'm also worried about readability and assumptions people will make – it could easily get confused for a member of a struct named await.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 10, 2018

Possibility to distinguish task start and task await is really important.

For what it's worth, the implicit version does do this. It was discussed to death both in the RFC thread and in the internals thread, so I won't go into a lot of detail here, but the basic idea is only that it moves the explicitness from the task await to task construction- it doesn't introduce any new implicitness.

Your example would look something like this:

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

This is what I meant by "for this to work, the act of constructing a future must also be made explicit." It's very similar to working with threads in sync code- calling a function always waits for it to complete before resuming the caller, and there are separate tools for introducing concurrency. For example, closures and thread::spawn/join correspond to async blocks and join_all/select/etc.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 10, 2018

For what it's worth, the implicit version does do this. It was discussed to death both in the RFC thread and in the internals thread, so I won't go into a lot of detail here, but the basic idea is only that it moves the explicitness from the task await to task construction- it doesn't introduce any new implicitness.

I believe it does. I can't see here what flow would be in this function, where is points where execution breaks until await is completed. I only see async block which says "hello, somewhere here there are async functions, try to find out which ones, you will be surprised!".

Another point: Rust tend to be a language where you can express everything, close to bare metal and so on. I'd like to provide some quite artificial code, but I think it illustrates the idea:

var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
   var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
   if (firstFinishedTask != b) // our task is timeouted
      throw new Exception(); // doing something
   // more logic here
}
else
{
   // more logic here
}

Rust always tends to provide full control over what's happening. await allow you to specify points where continuation process. It also allows you to unwrap a value inside future. If you allows implicit conversion on use side, it has several implications:

  1. First of all, you have to write some dirty code to just emulate this behaviour.
  2. Now RLS and IDEs should expect that our value is either Future<T> or awaited T itself. It's not an issue with keywords - it it exists, then result is T, otherwise it's Future<T>
  3. It makes code harder to understand. In you example I don't see why it does interrupt execution at get_status_updates line, but it doesn't on get_status_update. They are quite similar to each other. So it's either doesn't work the way original code was or it's so much complicated that I can't see it even when I'm quite familiar with the subject. Both alternatives don't make this option a favor.
@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 10, 2018

I can't see here what flow would be in this function, where is points where execution breaks until await is completed.

Yes, this is what I meant by "this makes suspension points harder to see." If you read the linked internals thread, I made an argument for why this isn't that big of a problem. You don't have to write any new code, you just put the annotations in a different place (async blocks instead of awaited expressions). IDEs have no problem telling what the type is (it's always T for function calls and Future<Output=T> for async blocks).

I will also note that your understanding is probably wrong regardless of the syntax. Rust's async functions do not run any code at all until they are awaited in some way, so your b.Status != TaskStatus.RanToCompletion check will always pass. This was also discussed to death in the RFC thread, if you're interested in why it works this way.

In you example I don't see why it does interrupt execution at get_status_updates line, but it doesn't on get_status_update. They are quite similar to each other.

It does interrupt execution in both places. The key is that async blocks don't run until they are awaited, because this is true of all futures in Rust, as I described above. In my example, get_statuses calls (and thus awaits) get_status_updates, then in the loop it constructs (but does not await) count futures, then it calls (and thus awaits) join_all, at which point those futures concurrently call (and thus await) get_status_update.

The only difference with your example is when exactly the futures start running- in yours, it's during the loop; in mine, it's during join_all. But this is a fundamental part of how Rust futures work, not anything to do with the implicit syntax or even with async/await at all.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 10, 2018

I will also note that your understanding is probably wrong regardless of the syntax. Rust's async functions do not run any code at all until they are awaited in some way, so your b.Status != TaskStatus.RanToCompletion check will always pass.

Yes, C# tasks are executed synchronously until first suspension point. Thank you for pointing that out.
However, it doesn't really matter because I still should be able to run some task in background while executing the rest of the method and then check if background task is finished. E.g. it could be

var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the same

I've got your idea about async blocks and as I see they are the same beast, but with more disadvantages. In original proposal each async task is paired with await. With async blocks each task would be paired with async block at construction point, so we are in almost same situation as before (1:1 relationship), but even a bit worse, because it feel more unnatural, and harder to understand, because callsite behavior becomes context-depended. With await I can see let a = foo() or let b = await foo() and I would know it this task is just constructed or constructed and awaited. If i see let a = foo() with async blocks I have to look if there is some async above, if I get you right, because in this case

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

We are awaiting for all tasks at once while here

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Isn't "just a construction" anymore
        task.push({
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }
    tasks 
}

We are executing them one be one.

Thus I can't say what's exact behavior of this part:

let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))

Without having more context.

And things get more weird with nested blocks. Not to mention questions about tooling etc.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 11, 2018

callsite behavior becomes context-depended

This is already true with normal sync code and closures. For example:

// Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
    let data = do_something_synchronous();
    StatusUpdate { data }
});

vs

// Execute a block, immediately running `do_something_synchronous()`:
task.push({
    let data = do_something_synchronous();
    StatusUpdate { data }
});

One other thing that you should note from the full implicit await proposal is that you can't call async fns from non-async contexts. This means that the function call syntax some_function(arg1, arg2, etc) always runs some_function's body to completion before the caller continues, regardless of whether some_function is async. So entry into an async context is always marked explicitly, and function call syntax is actually more consistent.

@jplatte

This comment has been minimized.

Copy link
Contributor

jplatte commented May 11, 2018

Regarding await syntax: What about a macro with method syntax? I can't find an actual RFC for allowing this, but I've found a few discussions (1, 2) on reddit so the idea is not unprecedented. This would allow await to work in postfix position without making it a keyword / introducing new syntax for only this feature.

// Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()?
@fdietze

This comment has been minimized.

Copy link

fdietze commented May 11, 2018

There is a library from the Scala-world which simplifies monad compositions: http://monadless.io

Maybe some ideas are interesting for Rust.

quote from the docs:

Most mainstream languages have support for asynchronous programming using the async/await idiom or are implementing it (e.g. F#, C#/VB, Javascript, Python, Swift). Although useful, async/await is usually tied to a particular monad that represents asynchronous computations (Task, Future, etc.).

This library implements a solution similar to async/await but generalized to any monad type. This generalization is a major factor considering that some codebases use other monads like Task in addition to Future for asynchronous computations.

Given a monad M, the generalization uses the concept of lifting regular values to a monad (T => M[T]) and unlifting values from a monad instance (M[T] => T). > Example usage:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Note that lift corresponds to async and unlift to await.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 11, 2018

This is already true with normal sync code and closures. For example:

I see several differences here:

  1. Lambda context is unavoidable, but it's not for await. With await we don't have a context, with async we have to have one. The former wins, because it provide the same features, but require knowing less about the code.
  2. Lambdas tends to be short, several lines at most so we see the entire body at once, and simple. async functions may be quite big (as big, as regular functions) and complicated.
  3. Lambdas are rarely nested (except for then calls, but it's await is proposed for), async blocks are nested frequently.

One other thing that you should note from the full implicit await proposal is that you can't call async fns from non-async contexts.

Hmm, I didn't notice that. It doesn't sound good, because in my practice you often want to run async from non-async context. In C# async is just a keyword that allows compiler to rewrite function body, it doesn't affect function interface in any way so async Task<Foo> and Task<Foo> are completely interchangeable, and it decouples implementation and API.

Sometimes you may want to to block on async task, e.g when you want to call some network API from main. You have to block (otherwise you return to the OS and the program ends) but you have to run async HTTP request. I'm not sure what solution could be here except hacking main to allow it to be async as well as we do with Result main return type, if you cannot call it from non-async main.

Another consideration in favor of current await is how it works in other popular language (as noted by @fdietze ). It makes it easier to migrate from other language such as C#/TypeScript/JS/Python and thus is a better approach in terms of drumming up new people.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 11, 2018

I see several differences here

You should also realize that the main RFC already has async blocks, with the same semantics as the implicit version, then.

It doesn't sound good, because in my practice you often want to run async from non-async context.

This is not an issue. You can still use async blocks in non-async contexts (which is fine because they just evaluate to a F: Future as always), and you can still spawn or block on futures using exactly the same API as before.

You just can't call async fns, but instead wrap the call to them in an async block- as you do regardless of the context you're in, if you want a F: Future out of it.

async is just a keyword that allows compiler to rewrite function body, it doesn't affect function interface in any way

Yes, this is is a legitimate difference between the proposals. It was also covered in the internals thread. Arguably, having different interfaces for the two is useful because it shows you that the async fn version will not run any code as part of construction, while the -> impl Future version may e.g. initiate a request before giving you a F: Future. It also makes async fns more consistent with normal fns, in that calling something declared as -> T will always give you a T, regardless of whether it's async.

(You should also note that in Rust there is still quite a leap between async fn and the Future-returning version, as described in the RFC. The async fn version does not mention Future anywhere in its signature; and the manual version requires impl Trait, which carries with it some problems to do with lifetimes. This is, in fact, part of the motivation for async fn to begin with.)

It makes it easier to migrate from other language such as C#/TypeScript/JS/Python

This is an advantage only for the literal await future syntax, which is fairly problematic on its own in Rust. Anything else we might end up with also has a mismatch with those languages, while implicit await at least has a) similarities with Kotlin and b) similarities with synchronous, thread-based code.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 11, 2018

Yes, this is is a legitimate difference between the proposals. It was also covered in the internals thread. Arguably, having different interfaces for the two is useful

I'd say having different interfaces for the two has some advantages, because having API depended on implementation detail doesn't sound good to me. For example, you are writing a contract that is simply delegating a call to internal future

fn foo(&self) -> Future<T> {
   self.myService.foo()
}

And then you just want to add some logging

async fn foo(&self) -> T {
   let result = await self.myService.foo();
   self.logger.log("foo executed with result {}.", result);
   result
}

And it becomes a breaking change. Whoa?

This is an advantage only for the literal await future syntax, which is fairly problematic on its own in Rust. Anything else we might end up with also has a mismatch with those languages, while implicit await at least has a) similarities with Kotlin and b) similarities with synchronous, thread-based code.

It's an advantage for any await syntax, await foo/foo await/foo@/foo.await/... once you get that it's the same thing, the only difference is that you place it before/after or have a sigil instead of keyword.

You should also note that in Rust there is still quite a leap between async fn and the Future-returning version, as described in the RFC

I know it and it disquiets me a lot.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented May 11, 2018

And it becomes a breaking change.

You can get around that by returning an async block. Under the implicit await proposal, your example looks like this:

fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
    async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}

And with logging:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        result
    }
}

The bigger issue with having this distinction arises during the transition of the ecosystem from manual future implementations and combinators (the only way today) to async/await. But even then the proposal allows you to keep the old interface around and provide a new async one alongside it. C# is full of that pattern, for example.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented May 12, 2018

Well, that sounds reasonable.

However, I do believe such implicitness (we don't see if foo() here is async or sync function) lead to the same problems that arised in protocols such as COM+ and was a reason for WCF being implemented as it was. People had problems when async remote requests were looking like simple methods calls.

This code looks perfectly fine except I can't see if some request if async or sync. I believe that it's important information. For example:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        let bars: Vec<Bar> = Vec::new();
        for i in 0..100 {
           bars.push(self.my_other_service.bar(i, result));
        }
        result
    }
}

It's crucial to know if bar is sync or async function. I often see await in the loop as a marker that this code have to be changed to achieve better throughout load and performance. This is a code I reviewed yesterday (code is suboptimal, but it's one of review iterations):

image

As you can see, I easily spotted that we have a looping await here and I asked to change it. When change was committed we got 3x page load speedup. Without await I could easily overlook this misbehaviour.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jan 15, 2019

Granting that every option has a downside, and that one of them should nonetheless end up being chosen... one thing that bothers me about foo.await is that, even if we assume that it won't literally be mistaken for a struct field, it still looks like accessing a struct field. The connotation of field access is that nothing particularly impactful is happening -- it's one of the least-effectful operations in Rust. Meanwhile awaiting is highly impactful, one of the most side-effecting operations (it both performs the I/O operations built up in the Future and has control flow effects). So when I read foo.await.method(), my brain is telling me to kind of skip over the .await because it's relatively uninteresting, and I have to use attention and effort to manually override that instinct.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Jan 15, 2019

it still looks like accessing a struct field.

@glaebhoerl You make good points; however, does syntax highlighting have no/insufficient impact on what it looks like and the way your brain processes things? At least for me color and boldness matters a great deal when reading code so I wouldn't skip over .await that has a different color than the rest of the things.

@lnicola

This comment has been minimized.

Copy link
Contributor

lnicola commented Jan 15, 2019

The connotation of field access is that nothing particularly impactful is happening -- it's one of the least-effectful operations in Rust. Meanwhile awaiting is highly impactful, one of the most side-effecting operations (it both performs the I/O operations built up in the Future and has control flow effects).

I strongly agree with this. await is a control flow operation like break or return, and should be explicit. The proposed postfix notation feels unnatural, like Python's if: compare if c { e1 } else { e2 } to e1 if c else e2. Seeing the operator at the end makes you do a double-take, regardless of any syntax highlighting.

I also don't see how e.await is more consistent with the Rust syntax than await!(e) or await e. There's no other postfix keyword, and since one of the ideas was to special-case it in the parser, I don't think that's a proof of being consistent.

There's also the familiarity issue @withoutboats mentioned. We can choose weird and wonderful syntax if it has some wonderful benefits. Does a postfix await have them, though?

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jan 15, 2019

does syntax highlighting have no/insufficient impact on what it looks like and the way your brain processes things?

(Good question, I'm sure it would have some impact, but it's hard to guess how much without actually trying it (and substituting a different keyword only gets so far). While we're on the subject... a long time ago I mentioned that I think syntax highlighting should highlight all operators with control flow effects (return, break, continue, ?... and now await) in some special extra-distinctive color, but I'm not in charge of the syntax highlighting for anything and don't know if anyone actually does this.)

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Jan 15, 2019

I strongly agree with this. await is a control flow operation like break or return, and should be explicit.

We agree. The notations foo.await, foo await, foo#, ... are explicit. There's no implicit await being done.

I also don't see how e.await is more consistent with the Rust syntax than await!(e) or await e.

The syntax e.await per se isn't consistent with Rust syntax but postfix generally fits better with ? and how Rust APIs are structured (methods are preferred over free functions).

The await e? syntax, if associated as (await e)? is completely inconsistent with how break and return associate. await!(e) is also inconsistent since we don't have macros for control flow and it also has the same problem as other prefix methods.

There's no other postfix keyword, and since one of the ideas was to special-case it in the parser, I don't think that's a proof of being consistent.

I don't think you actually need to change libsyntax at all for .await since it should already be handled as a field operation. The logic would rather be dealt with in resolve or HIR where you translate it to a special construct.

We can choose weird and wonderful syntax if it has some wonderful benefits. Does a postfix await have them, though?

As aforementioned, I argue it does due to method chaining and Rust's preference for method calls.

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented Jan 15, 2019

I don't think you actually need to change libsyntax at all for .await since it should already be handled as a field operation.

This is fun.
So the idea is to reuse the self/super/...'s approach, but for fields rather than for path segments.

This effectively makes await a path segment keyword though (since it goes through resolution), so you may want to prohibit raw identifiers for it.

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}
@lnicola

This comment has been minimized.

Copy link
Contributor

lnicola commented Jan 15, 2019

There's no implicit await being done.

The idea came up a couple of times on this thread (the "implicit await" proposal).

we don't have macros for control flow

There is try! (which served its purpose pretty well) and arguably the deprecated select!. Note that await is "stronger" than return, so it's not unreasonable to expect it to be more visible in the code than ?'s return.

I argue it does due to method chaining and Rust's preference for method calls.

It also has a (more noticeable) preference for prefix control flow operators.

The await e? syntax, if associated as (await e)? is completely inconsistent with how break and return associate.

I prefer await!(e)?, await { e }? or maybe even { await e }? -- I don't think I've seen the latter discussed, and I'm not sure if it works.


I admit might have a left-to-right bias. Note

@novacrazy

This comment has been minimized.

Copy link

novacrazy commented Jan 15, 2019

My opinion on this seems to change every time I look at the issue, as if playing Devil’s advocate to myself. Part of that is because I’m so used to writing my own futures and state machines. A custom future with poll is totally normal.

Perhaps this should be thought of another way.

To me, zero-cost abstractions in Rust refers to two things: zero-cost at runtime, and more importantly zero-cost mentally.

I can very easily reason about most abstractions in Rust, including futures, because they are just state machines.

To this end, a simple solution should exist that introduces as little magic to the user. Sigils especially are a bad idea, as they feel unnecessarily magical. This includes .await magic fields.

Perhaps the best solution is the easiest one, the original await! macro.

@rpjohnst

This comment has been minimized.

Copy link
Contributor

rpjohnst commented Jan 15, 2019

So with all due respect to those other languages, internal consistency in Rust seems more important and I don't think prefix syntax fits Rust either in terms of precedence or in how APIs are structured.

I don't see how...? await(foo)?/await { foo }? seems totally fine in terms of operator precedence and how APIs are structured in Rust- its downside is the wordiness of parens and (depending on your perspective) chaining, not breaking precedent or being confusing.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Jan 15, 2019

There is try! (which served its purpose pretty well) and arguably the deprecated select!.

I think the operative word here is deprecated. Using try!(...) is a hard error on Rust 2018. It is a hard error now because we introduced a better, first-class, and postfix syntax.

Note that await is "stronger" than return, so it's not unreasonable to expect it to be more visible in the code than ?'s return.

The ? operator can likewise be side-effecting (through other implementations than for Result) and performs control flow so it's quite "strong" as well. When it was discussed, ? was accused of "hiding a return" and being easy to overlook. I think that prediction completely failed to come true. The situation re. await seems quite similar to me.

It also has a (more noticeable) preference for prefix control flow operators.

Those prefix control flow operators are typed at ! type. Meanwhile, the other control flow operator ? that takes a context impl Try<Ok = T, ...> and gives you a T is postfix.

I don't see how...? await(foo)?/await { foo }? seems totally fine in terms of operator precedence and how APIs are structured in Rust-

The await(foo) syntax is not the same as await foo if parenthesis is required for the former and not for the latter. The former is unprecedented, the latter has precedence issues wrt. ? as we've discussed here, on boat's blog post, and on Discord. The await { foo } syntax is problematic for other reasons (see #50547 (comment)).

its downside is the wordiness of parens and (depending on your perspective) chaining, not breaking precedent or being confusing.

This is what I mean by "APIs are structured". I think methods and method chaining are common and idiomatic in Rust. The prefix and block syntaxes compose poorly with those and with ?.

@seanmonstar

This comment has been minimized.

Copy link
Contributor

seanmonstar commented Jan 15, 2019

I may be in the minority with this opinion, and if so, ignore me:

Would it be fair to move the prefix-vs-postfix discussion to an Internals thread, and then simply come back here with the outcome? That way we can keep the tracking issue to tracking the status of the feature?

@cramertj

This comment has been minimized.

Copy link
Member

cramertj commented Jan 15, 2019

@seanmonstar Yeah, I strongly sympathize with the desire to limit discussion on tracking issues and have issues that are really just status updates. This is one of the issues I hope we can tackle with some revisions to how we manage the RFC process and issues in general. For now, I've opened a new issue here for us to use for discussion.

IMPORTANT TO ALL: further await syntax discussion should go here.

@rust-lang rust-lang locked as off topic and limited conversation to collaborators Jan 15, 2019

@cramertj

This comment has been minimized.

Copy link
Member

cramertj commented Jan 15, 2019

Temporarily locking for a day to ensure that future discussion about await syntax occurs on the appropriate issue.

@rust-lang rust-lang unlocked this conversation Jan 17, 2019

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jan 18, 2019

@jens1o jens1o referenced this issue Jan 26, 2019

Open

Make it async #4

@ErichDonGubler

This comment has been minimized.

Copy link

ErichDonGubler commented Feb 25, 2019

Link updates: Looks like #53259 and #53447 have been closed. rust-lang-nursery/futures-rs#1199 appears to have been moved to #53548.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 1, 2019

Async-await status report:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


I wanted to post a quick update on the status of the async-await
effort. The short version is that we're in the home stretch for
some kind of stabilization, but there remain some significant
questions to overcome.

Announcing the implementation working group

As part of this push, I'm happy to announce we've formed a
async-await implementation working group. This working group
is part of the whole async-await effort, but focused on the
implementation, and is part of the compiler team. If you'd like to
help get async-await over the finish line, we've got a list of issues
where we'd definitely like help (read on).

If you are interested in taking part, we have an "office hours"
scheduled for Tuesday (see the compiler team calendar)
-- if you
can show up then on Zulip, it'd be ideal! (But if not, just pop in any
time.)

...

@jethrogb

This comment has been minimized.

Copy link
Contributor

jethrogb commented Mar 7, 2019

@ry

This comment has been minimized.

Copy link

ry commented Mar 12, 2019

When will std::future::Future be in stable? Does it have to wait for async await? I think it's a very nice design and would like to start porting code to it. (Is there a shim to use it in stable?)

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Mar 12, 2019

@ry see the fresh tracking issue for it: #59113

@Ekleog

This comment has been minimized.

Copy link

Ekleog commented Mar 16, 2019

Another compiler issue for async/await: #59245

Also note that rust-lang-nursery/futures-rs#1199 in the top post can be checked off, as it's now fixed.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Mar 21, 2019

It looks like there's an issue with HRLB and async closures: #59337. (Though, re-skimming the RFC it doesn't actually specify that async closures are subject to the same argument lifetime capture that async function have).

@cramertj

This comment has been minimized.

Copy link
Member

cramertj commented Mar 21, 2019

Yeah, async closures have a bunch of issues and shouldn't be included in the initial round of stabilization. The current behavior can be emulated with a closure + async block, and in the future I'd love to see a version that allowed referencing the closure's upvars from the returned future.

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.