How will Promise.all be implemented?

Continuing the discussion from A final proposal for await syntax:

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.

How do people envision implementing this function in Rust? I searched for Promise.all on Resolve `await` syntax · Issue #57640 · rust-lang/rust · GitHub, but didn't find anything.

The above used the traditional prefix keyword syntax, but the .await field syntax could work the same.

1 Like

I think that’s https://docs.rs/futures/0.2.1/futures/trait.FutureExt.html#method.join (or, if you have more that 5 futures, https://docs.rs/futures/0.2.1/futures/future/fn.join_all.html) Read @Matthias247’s reply below

Can we implement Future for tuples of Futures? Do we want to?

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.

6 Likes

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.

2 Likes

Ah, right. Why is there no IntoFuture trait, though?

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.

7 Likes

To clarify something, you pointed to the Futures 0.1 docs (which are incompatible with async/await).

These are the docs for Futures 0.3. In addition, the join methods have been replaced with functions (there’s also join2, .join3, join4, and join5).

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).

So in that case you should use try_join!, or the try_join 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).

5 Likes

(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).

1 Like

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 exactly what select does: it runs two Futures in parallel, and whichever completes first is returned (and the other one is cancelled).

It returns an Either enum, so that way you know which Future returned first.

If you want to wait for more than 2 Futures, you can just chain select, like this:

let x = select(foo, select(bar, qux)).await;

And then pattern match on the Either.

There’s also select_all (for selecting from an iterator of Futures of the same type), and select_ok (for selecting over fallible Futures).

1 Like

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.

I touched upon the same point here:

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.

5 Likes

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)

They are all combinators that don’t do anything till you await their results.

4 Likes

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_join can be functions, but await cannot 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.

3 Likes

Yeah, I've been slowly reading/skimming my way through this discussion: Resolve `await` syntax · Issue #57640 · rust-lang/rust · GitHub -- I see that this has all been discussed several times already there.

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.

1 Like

Indeed, the Rust team is very thorough, and knowledgeable about many languages.

Official C# team members have even participated in the discussions, explaining why they designed async/await the way that they did.

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).

5 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.