Relying on syntax highlighting is a huge red flag for me. I should be able to open up rust code in something like notepad or cat
from a terminal and quickly be able to see what's going on.
But what about if you want to join 4 futures, how about 5 or 6? This could go on an on, and without variadic generics, we can't do any better than a macro or nested tuples. Now a macro could easily convert from nested tuples to the flattened version, and we can do that now.
I don't think this would be a good idea, it seems to brittle.
In C#, I yesterday (literally -- I just checked) gave the "you should do that in parallel" feedback for this code (domain object names obfuscated, but the code is otherwise untouched):
var sprockets = await this.ExtractContent<SprocketContent>(SprocketPath);
var widgets = await this.ExtractContent<WidgetContent>(WidgetPath);
var spanners = await this.ExtractContent<SpannerContent>(SpannerPath);
So I think prefix await isn't immune to "just await without thinking about it" either.
That said, it's a pain in C# to do this differently, because Task.WhenAll
only supports the homogeneous case. If I understand How will Promise.all be implemented? - #4 by Matthias247 correctly, then this will be way better in Rust -- whether prefix or postfix -- as it could be
let (sprockets, widgets, spanners) = join!(
self.extract_content::<SprocketContent>(SPROCKET_PATH),
self.extract_content::<WidgetContent>(WIDGET_PATH),
self.extract_content::<SpannerContent>(SPANNER_PATH),
).await;
+1, and I wonder if that actually makes .await
less noticeable than with no highlighting, as one's brain starts relying on the colours for mental parsing shortcuts.
I am suprised that no one is talking about UCS macro in this thread. I’ve noticed that a large part of community in reddit has reached consensus on postfix macro fashion powered by unified call syntax + prefix madatory.
Besides, there is a unofficial survey, which also shows favors of postfix macro among all proposals (the one with biggest proportion of ‘perfect’ option).
In brief, with UCS-macro, postfix await will be not magical; it’ll be trival, orthogonal, user definable (except that await is a keyword). And the most importantly, it completes the chaining story in rust so that we don’t need ? for Try, magical await field access for async and new magical syntax for something may appear in the future. (ps: ? syntax is cute, I love it)
It doesn’t require syntax highlighting. And if you don’t have syntax highlighting at all, I think it still reads just fine.
Having some syntax highlighting that is incomplete (and doesn’t know about all keywords) is harmful: highlighting some keywords gives a signal that keywords will be highlighted, making un-highlighted keywords easier to miss.
People aren't talking about it because it's already been discussed to death (months ago), and rejected by the Rust team (for await
). Perhaps postfix macros will be implemented in the future, but not right now.
A potentially very interesting area to explore is a lint that detects missed opportunities for concurrency. It could suggest join
ing over awaits where the awaits don’t depend on each other.
I can’t say how easy that is to implement without producing a lot of noise, and this would go into clippy rather than the compiler.
But it could be quite useful.
We may not have universal postfix macro in the first place; we can accept .await!() as a special case just like .await field access and reserves the possibility in future.
One thing interesting is in a repo which demos what each fashion of await syntax will look like in a real project, people found it different from their imagination that they would rarely chain awaits(await on a future returned from other await) and they suprisingly found it comfortable to work with prefix madatory in the most time because of the former. So unlike ?
, we may not starve for a postfix await; we just want some form of it to be stabilized, so why not having the prefix madatory(which will be added anyway) now and add whatever we like later?
That seems to be about polling futures once and then awaiting them later. The end result is still a future. I'm talking about modifying order of evaluation in an expression that depends on the result of awaiting a future so that, if there's multiple independent awaits, they can be implicitly joined, along the lines of what @josh was suggesting.
A macro could probably work, though I'm not sure how it would handle a mix of fallible and infallible futures in the kind of mechanical transformation needed to make it work (pulling out subexpressions into their own async blocks so they can be joined before awaiting them to pass to the parent expression). It could be called lazy_async!
or something. I do think this would need to be a distinct syntax since messing with order of execution unexpectedly could cause problems.
Indeed. The first await
is to spawn the Future so it runs in the background (this is necessary). The second await
is to actually wait for the result of the Future.
Therefore, Defer
allows you to run multiple Futures in parallel, and then await
the result only when needed (as I showed in my post).
There might be other ways to implement it, but my point still remains that this is entirely possible in a crate, no need for it to be built-in.
This is not how futures work in Rust in general. Your wrapper allows the wrapped future to do a single "tick" of async work before it stops polling it and waits till the returned DeferredState
is polled. For a few futures like a TCP connect
this works as they only have a single "tick" of work to do, but for something like writing data to a socket this will give one kernel buffers worth of work to the kernel to do and not do any more till the final future is polled.
If you want to run a future in the background you must use something like RemoteHandle
along with Spawn
(which are available together as spawn_with_handle
) to spawn the future as a new top-level task and provide a channel to get its return value.
There are some libraries that have chosen to implicitly do something similar to spawn_with_handle
with futures they return in their public API. In those cases you don't even need something like Defer
to start them as they start as part of the synchronous call to create the future.
I don't think "automatically transformed" would be a good idea because of the issues that have already been noted about accidentally put yield points at the wrong place causing bugs. But having tuples implement some IntoFuture
trait that's used by await
and a "splatting" operator for tuples that has come up in relation to variadic arguments (represented here by …
) gives a succinct (if not exactly readable) way to write this
baz(…(foo(), bar()).await)
After adding initial syntax support for async/await
in Sublime Text, I got this:
Do you find it barely visible?
Snippet in case one want to try it oneself
#![feature(async_await)]
use wasm_bingen::prelude::*;
use wasm_bingen_futures::futures_0_3::*;
#[wasm_bingen(start)]
pub fn start() {
spawn_local(async {
let x = delay_str("Hi!").await;
assert_eq!(x, "Hi!");
})
}
async fn delay_str(s: &str) -> String {
let val = JsValue::from(s);
let future = JsFuture::from(js_sys::Promise::resolve(&val));
future
.await
.expect("promise resolve OK")
.as_string()
.expect("JS value is a string")
}
Of course syntax highlighting should be able to fix this. But there are situations where such luxury is not possible:
git diff
diff
cat
- notepad
Incorrect highlighting will always mislead, however. The original tweeted example highlighted .await
as a field, so your processing that leans on the colors can just say “it looks like a field”. When no syntax highlighting is present, your comprehension is a bit slower, so you have a chance to say “wait, that’s a keyword” (though “it looks like a field” is still present, and can be minimized by formatting it on the same line rather than the next).
So this is the code snippet without syntax highlighting.
#![feature(async_await)]
use wasm_bingen::prelude::*;
use wasm_bingen_futures::futures_0_3::*;
#[wasm_bingen(start)]
pub fn start() {
spawn_local(async {
let x = delay_str("Hi!").await;
assert_eq!(x, "Hi!");
})
}
async fn delay_str(s: &str) -> String {
let val = JsValue::from(s);
let future = JsFuture::from(js_sys::Promise::resolve(&val));
future
.await
.expect("promise resolve OK")
.as_string()
.expect("JS value is a string")
}
Even without syntax highlighting, most keywords are still distinguishable from field access and method call as they only appear in a special placement of a particular syntax structure:
- only
fn
can appear before function names - only
pub
can appear before declarations - only
let
orlet mut
can appear before variable names in variable declarations - etc.
Most things placed after dot are field access and method calls, so it is easy to overlook .await
when syntax highlight is not supported.
Just for completeness, here’s how I’d currently format the snippet:
#[wasm_bingen(start)]
pub fn start() {
spawn_local(async {
let x = delay_str("Hi!").await;
assert_eq!(x, "Hi!");
})
}
async fn delay_str(s: &str) -> String {
let val = JsValue::from(s);
JsFuture::from(Promise::resolve(&val)).await
.expect("promise resolve OK")
.as_string()
.expect("JS value is a string")
}
IMO, with syntax highlighting it’s even worse:
- the first
await
looks almost the same asasync
which requires me to look more closely to recognize each of them - my eyes predicts that the second
await
is rathermatch
orloop
construct which always turns to be the wrong impression
Contrarily, it doesn’t happen with postfix sigil syntax:
#![feature(async_await)]
use wasm_bingen::prelude::*;
use wasm_bingen_futures::futures_0_3::*;
#[wasm_bingen(start)]
pub fn start() {
spawn_local(async {
let x = delay_str("Hi!")@;
assert_eq!(x, "Hi!");
})
}
async fn delay_str(s: &str) -> String {
let val = JsValue::from(s);
let future = JsFuture::from(js_sys::Promise::resolve(&val));
future@.expect("promise resolve OK")
.as_string()
.expect("JS value is a string")
}
And for me it’s very frustrating that it’s just sacrificed in sake of “familiarity” ideology
I would expect this to be worse with prefix await, not better, because await {
and async {
would both be valid code.
Which, come to think of it, is another difference between Rust and C#: the latter doesn't have async
blocks.
Another thing, from a design perspective, we should add thing that is easy to change. And I don’t think future.await
is easy to change (what if we want future.await()
or future.await!()
later on), but future@await
, future@
, and await future
are all easy to change/remove.