When I've used futures or promises in the past, it was very common for me to call 2-3 async functionsin a row and then await all futures at once. That way the calls would run in parallel in the background.
In fact, I think it would be really nice if I could write
let a = fetch("Alice");
let b = fetch("Bob");
let users = await (a, b);
to fetch user data in parallel for Alice and Bob. It would be like Promise.all in JavaScript.
With futures 0.3 the preferred way is the join! macro.
FutureExt::join is still around, but I see next to no use-case for it anymore (same for all other combinators). join_all is useful if the list of Futures that need to be awaited is not static.
If we had IntoFuture that could make sense to implement for a tuple, it’s impossible to directly implement Future for a tuple as it’s lacking somewhere to store the resolved values while the rest of the futures complete.
Awesome! Thanks for the link. I'm very happy to see that this is implemented as a "user-space" macro.
A constant theme throughout the await syntax discussions has been the need for it to be chainable. However, it's my experience that combinator macros like join!(...), try_join!, and select! are much more important.
Something else I need to mention is that with Futures 0.3, Futures are no longer required to have an error type. That means that Futures which error need to return Result (just like regular functions).
I should also mention FuturesUnordered which is a way to dynamically add multiple Futures and then wait for all of them to finish. In some cases it can be faster than join_all.
Also, there is a reason to use the functions rather than the join! or try_join! macros. The macros only accept identifiers, so you need to do this:
let a = some_future();
let b = some_other_future();
let (a, b) = join!(a, b);
But with the functions, you can do this:
let (a, b) = join(
some_future(),
some_other_future(),
).await;
Visually, this makes it more clear that the Futures are being run in parallel (unlike the macro version which gives the illusion that they are sequential).
(That’s actually a consequence of a tag being pushed to the wrong branch, those are the 0.3 docs, the URL just includes 0.1.27. I was going to delete that folder but since it’s been linked to I thought it better to leave it online. I’ve removed it from the index now so that hopefully it won’t be linked to again).
For some reason I expected this to be like select(), but that doesn’t seem like the case? Will there be support for a version of join that awaits all the futures simultaneously, and returns the result of the version that returns first? (Typing this seems like a problem, since you want to turn for<Ts..> (Future<Output=Ts>...) into for<Ts...> enum(Ts...)…)
That's an interesting development. I've worked with Deferred in Python and Promise in JavaScript and they both follow the same model where the Future can be in one of three states: pending, successful (resolved), or failed (errored). There are (chains of) callbacks associated with both the successful and the failure state. If an error occurs in a success handler, the flow switches to the error handlers. An error handler can then deal with the error and the flow switches back to the normal callbacks, otherwise the next error handler is called.
The net result of this is that the error handling becomes asynchronous. The way the async/await discussion is presented in the final proposal (in particular the "error handling problem") seems to indicate that error handling for Rust futures should be synchronous. That is, you get a Future<Result>, you await it, you look at the error, and then you continue with your work.
Yes, that is correct. It's not a new development, it was done many many months ago.
The end result is that we can now correctly use Futures which don't error, and we gain significantly increased consistency and orthogonality.
It means that async functions aren't much different from normal functions, other than their ability to yield. A normal function returns a Result and handles errors with ?, and an async function does the same thing.
This makes perfect sense for Rust, which has explicit error handling. JavaScript Promises make perfect sense in JavaScript, which has implicit exceptions.
I'd also like to note that Rust is doing the same thing as JavaScript Promises (except Rust is doing it explicitly).
With JavaScript Promises, it waits for the Promise to resolve, then synchronously checks whether it's an error or not, then synchronously calls the appropriate callback handler.
This all happens within a single tick, on the microtask queue (which is why it's synchronous, not asynchronous).
Let's compare that to Rust. It waits for the Future to resolve, then synchronously checks whether it's an error or not (using pattern matching).
Essentially, Rust is just cutting out the final "call the appropriate callback" step, everything prior to that is the same.
Looking at the docs with join, try_join, select, ready, etc. They are all normal function calls I feel like await(future) would actually fit in well. (Never thought I’d say that)
I don't really want to get into a syntax argument, since I think that's pretty off-topic in this thread, but what you're saying is that instead of this...
let (a, b) = try_join(
some_future(),
some_other_future(),
).await?;
...we should instead have this:
let (a, b) = await(try_join(
some_future(),
some_other_future(),
))?;
Personally, I consider the first one visually much more appealing. And it is objectively easier to type and understand (since it has fewer parentheses and it doesn't require you to jump back and forth).
Things like join and try_joincan be functions, but awaitcannot be a function. So making await stand out and look different can be an advantage, not a disadvantage.
Keep in mind that the join / try_join / etc. functions are usually combined with.await (as you can see in the above example), so it's better to think of foo().await as a single visual unit, similar to how .await? is a single visual unit.
Thanks for answering (again) here! It's really nice for me personally to compare the Rust implementation with other languages that I've used in the past. I hope it's also useful for others.
I like your other explanations here, it's reassuring to see that you've taken into account how languages like JavaScript to things.
The Rust team does a really good job, and I have a lot of respect for them. I think the only thing that could be done better is to have some sort of system for summarizing information, since right now the discussions are spread out all over the place, making it hard to find.
To be clear, I'm on the Rust Wasm WG, but I'm not a Rust Lang team member, so everything I say is just my own opinion. But I have a lot of experience with different languages (including 13 years of experience with JavaScript).