Explicit future construction, implicit await

@rpjohnst Are you proposing that async functions (like print() in the example above) should act differently than hand-written functions that return a future (like Timeout::form_secs()) when they’re used?

How would I sequentially execute and implicitly await the following code?

async fn function() {
    let future_a = print::new("hello world");
    let future_b = Timeout::from_secs(5);
    select(future_a, future_b)
}

I fixed various things in the code from your comment. Also, I’ve included a question. Was I correct in applying these fixes?

async fn my_fn1() { // with forward-compatible explicit await
    async_get_num(); // error for forward compatibility

    await!(async_get_num::new()); // explicit await, Fix1: Added missing ::new()

    println!("{}", await!(async_get_num::new())); // explicit await, Fix:2 Added missing ::new()

    let future = async { await!(async_get_num::new()); }; // Fix3: Added missing await and added missing ::new()

    await!(join_all(vec![future, async_get_num::new()]); // explicit await
}

async fn my_fn2() { // implicit await (future Rust)
    async_get_num(); // implicit await

    // explicit await still works (though now it's just the identity macro)
    await!(async_get_num());

    println!("{}", async_get_num()); // implicit await (prints i32)

    // construct a future, no inference because it was never ambiguous to begin with
    let future = async { async_get_num() }; // Q1: Why is this not awaited implicitly? Async blocks return a future
    join_all(vec![future, async_get_num::new()]); // implicit await
}
1 Like

Yes, and I also covered this in the rationale section under "eager futures."

Sequentially executing that code makes no sense, because it uses a Timeout with select. If you actually wanted to execute the two futures sequentially (that is, print "hello world" and then wait for five seconds), you would write this:

async fn function() {
    print("hello world"); // `print` is `async`, so calling it implicitly awaits
    Timeout::from_secs(5).await(); // `Timeout` is a `Future`, so you can `.await()` it if you need to make it happen "now."
}

No:

  • Fix1 and Fix2 are have the same behavior as what I wrote, but they are unnecessary. Leaving off the ::new() is what allows await! to become the identity macro later. (Note that this is not part of my actual proposal, only the "forwards compatibility" concept.)
  • Fix3 is partially correct- I did need an await! (again, for future compatibility only), which I have now added, but the ::new() is unnecessary as it is for Fix1 and Fix2.

The async_get_num() call is awaited implicitly- but only inside the async block. The async block itself is not awaited implicitly, because it is one of the two ways to explicitly construct a future value.

Think about what it would mean if async blocks were awaited implicitly- they would no longer be usable outside of async fns, and they would no longer behave any differently from simply writing their contents outside the block.

I suspect you are still confused about what exactly I mean by "implicit await."

  • I emphatically do not mean that any future evaluated in an async context should be immediately awaited.
  • I do mean that the some_async_fn() syntax changes meaning from "construct a future" to "construct and await a future, without ever exposing that future." To go along with this, I note that it is still possible to obtain an actual future value in several ways:
    • Constructing a hand-written future, as always.
    • Using an async block, which evaluates to a value whose type implements Future just like a closure evaluates to a value whose type implements Fn.
    • Optionally, use a new some_async_fn::new() function, which provides the old behavior of some_async_fn() without the need for async { .. }'s additional anonymous type.

That's a very good point. However, I'm not sure it changes anything about my reasoning. Differentiating the two forms (one that borrows its arguments until completion and the other that releases them after construction) via explicit syntax has the same benefits as differentiating whether an initial request is fired off.

Having two Futures-like traits (async and Future) is IMO very confusing. Does Kotlin have such a distinction? Anyway such a distinction destroys a major purpose of async functions: They’re supposed to be a convenient way to write a function that returns a future. Calling them is supposed to work exactly like ordinary functions that return a future.

2 Likes

I’m having a hard time understanding this proposal since there is no direct comparison between code in the current RFC proposal and this proposal. I’d like to see comparisons for these points:

  • How is an async fn declared and what is the actual return type?
  • How is an async fn called from another async fn?
  • How is an async fn called from a regular fn?
  • In either function, how can you use the returned value of an async fn as impl Future?

I’m strongly against any proposal that can’t be correctly expressed in the type system, considering both the Future and Fn/FnMut/FnOnce traits.

3 Likes

I do not propose this at all. As in the main proposal, async is a keyword that transforms a function or block body so that it can suspend, and Future is a trait that lets you interact with both async-transformed functions and hand-written implementations.

Kotlin does sort of have this distinction, but it's because they use suspend funs for both generators and async/await. I discussed a design like this here but it makes the interaction between futures and iterators much harder to deal with. @withoutboats addresses this decision both in that thread and in the main RFC.

I disagree. They are supposed to be a convenient way to construct futures, but that doesn't mean they have to literally return a future when using the function call syntax.

Any such distinction would make async Rust just unnecessarily complicated.

1 Like

@jethrogb Fair enough. This proposal has no problems being expressed in the type system:

Declaration remains identical to the main RFC:

async fn foo(a: A, b: B) -> C { .. }

In the main RFC, this desugars to this:

fn foo(a: A, b: B) -> impl Future<Output = C> { .. }

In this proposal, it desugars to this:

mod foo {
    struct Type { .. }
    fn new(a: A, b: B) -> Type { .. }
    impl Future for Type {
        type Output = C;
        fn poll(self: Pin<Self>, cx: &task::Context) -> Async<Self::Output> { .. }
    }
}

(This is not, however, the point of this proposal- this module doesn't have to be exposed, as I mentioned under unresolved questions.)

The main RFC describes the desugaring for await!(foo()) as this:

let mut future = IntoFuture::into_future(foo());
let mut pin = unsafe { Pin::new_unchecked(&mut future) };
loop {
    match Future::poll(Pin::borrow(&mut pin), &mut ctx) {
          Async::Ready(item) => break item,
          Async::Pending     => yield,
    }	
}

This proposal uses the same expansion, but instead of applying it to await!(future) it applies it to foo():

let mut future = foo::new();
let mut pin = unsafe { Pin::new_unchecked(&mut future) };
loop { /* ... as before ... */ }

You do not. Instead, both types of functions call some_async_fn::new() when they want an impl Future. This has the same behavior that the main RFC ascribes to some_async_fn()- it constructs the async fn's corresponding future but does not run it:

// main RFC:
fn main() {
    let future = some_async_fn();
    tokio::run(future);
}

// this proposal:
fn main() {
    let future = some_async_fn::new();
    tokio::run(future);
}
1 Like

@rpjohnst You should add to your comment above that also that you propose fn_that_returns_a_future().await() to await a future inside async functions. So that everything you propose is finally together in one post. Your post has cleared a lot of things up that were still unclear to me. (As stated in my previous comments I do not agree with various aspects of it, though)

Note: I’ve created my account today and I’ve now reached my 16 reply limit for today. Thus I’m now “MajorBreakfast2” for the time being

Rust has intentional syntax noise ? for returns in error handling, as a compromise between explicit handling and terseness of exceptions.

Having “hidden” returns caused by futures would be unusual for Rust. However, maybe another sigil can be invented as a shortcut for await? e.g. let x = @fetch_x();

2 Likes

This kind of difference sounds surprising, at least when thinking about it under the simplified mental model of "one of them puts the extra syntax here, the other one puts it there". Could you explain? Under the "main" proposal, why couldn't the same thing be accomplished by changing the desugaring of await!() (which is what corresponds to "directly calling it" in this proposal)?

...Is it because await!() has to work uniformly for both Futures as well as async fns, while your proposal distinguishes these?

I think it would be helpful to illustrate this - that is, a normal sync function call messing things up in the same way as an await - using an example or two.

(I think this could use some unpacking as well - it's not obvious to me what kind of warnings by who this is referencing.)

This difference is what wins us the clarity around when work is delayed vs when it is fired off immediately. Delayed futures will have a .await() or will be passed to some combinator; normal async fns will appear to directly return their final result.

(I think I understand what you mean here, but only because I already know what you're probably trying to say. Otherwise, I think it'd be helpful to be more explicit about what the significant difference between "delayed futures" and "normal async fns" is, and how distinguishing them gives us greater clarity about what exactly.)

(It's not at all obvious to me what this could look like, which may be OK - I've never implemented a leaf future by hand either.)

Would it be hacky to just legislate that an async { ... } block containing exactly a single call to an async fn shall have the same type as that async fn, rather than a fresh anonymous type? Thus replacing a syntactic special case (::new() or whatever) with a semantic one.

(Would it still need to have the ::Type part even then? It's a bit weird to have things which look like fns but are also used as modules, so it'd be nice to avoid that if it's possible.)

1 Like

@kornel A lot of await syntax alternatives were discussed in the RFC issue before the discussion was tabled to focus on other stuff. https://github.com/rust-lang/rfcs/pull/2394#issuecomment-383038441 We had:

  • await!(func()) and await!(func())?
  • await func() and (await func())? or await? func()
  • await:func() and await:func()?
  • func()~ and func()~?
  • func().await and func().await?

This thread is about having no keyword, though (implicit await)

They're not hidden returns per se- the rest of the function will still run, unlike with ? applied to Err. The danger is more that the hidden suspension will lead to invalidated state, but as I said this can already happen just in a normal function call.

Thanks, this makes your proposal significantly clearer to me.

@glaebhoerl Thanks a bunch for the review! I think I'm going to try to revise the proposal based on this and the confusion earlier in the thread. But I'll say a couple things now:

Yes, exactly. Explicit await bakes the await "calling convention" into the interface, because you can await a Future the same way you run an async fn. This proposal includes an explicit "bridge" between the two worlds (the Future::await method).

I suspect it might be possible to make a CPS transform work under the main RFC, by automatically inserting those bridges whenever an async fn is called outside of an await! expansion, but I'm not sure it would be worth the complexity in the implementation of await! or in understanding what code the compiler generates.

That would get rid of the need for ::new(), but ::Type would still be useful. It enables people to name the async fn's corresponding type, for storing in structs or implementing methods on.

I kind of like it, though- maybe we could drop the braces when we want the inner type, like this:

let future = async { .. }; // fresh anonymous type
let future = async some_async_fn(); // `some_async_fn`'s corresponding type

Part of the confusion in the comparison that I think is somewhat missed under Kotlin and this formulation vs the RFC is that an async or suspending function cannot be called from synchronous code directly.

Kotlin bridges the gap between the synchronous and asynchronous worlds by providing builder functions which can be called from non-suspending code, but they still accept a suspending lambda (closure) from which you can call your suspendable code. This formulation is attempting to achieve that similar builder pattern with some_async_fn::new().

One of Kotlin’s builders is called “async {…}” as @rpjohnst has mentioned before, and it’s what exposes the “.await()” function using a “Deferred” interface.

In Kotlin, “async {…}” implies I am going to run this suspending lambda right away, on either a default context or an explicit one, and so you must “.await()”, otherwise it will run to completion and not return a value to anyone. Whereas calling a suspendable function implies that my current function will suspend (or Yield in Rust) and then start running that function (similar to await!()), similar to blocking code in that it still conforms to the linear mental model of code I've been using up to that point.

I think @rpjohnst's other argument is that it's the breaking of linearity which causes confusion, as await!() still implies that my function has stopped until the other future completes.

Kotlin does not quite have an analogous type to Rust’s Future built-in. It’s “Continuation” interface is actually closer in relation to “Generator” in Rust, so in Kotlin, “Jobs” acts as a hybrid of Rust’s “Tasks” and “Future”. Like Generators in Rust currently, you never work directly with Continuations, but instead, you interact with the Job type.

1 Like

I’d like to challenge the assumption that linear await is the most common case in async fn.

In JavaScript-y syntax, I think the common case is the following:

const fileA = fs.read_async("fileA")
const fileB = fs.read_async("fileB")
return await process(await fileA, await fileB)

Note that this processes both async reads in parallel. This is, to me, the reason for using async fn: that you can get multiple tasks running at the same time that require waiting. Asynchronous IO is by far the most common case of async code.

However, Rust has decided it does not want to start computation until the async context is awaited, so this formulation requires some kind of join so both futures can be awaited concurrently. So, in fully qualified and macro syntax (don’t quote me this is free hand):

let file_a = async! { fs::read_async("fileA") };
let file_b = async! { fs::read_async("fileB") };
let [file_a, file_b] = await!(Future::all([file_a, file_b]));
await!(process(fileA, fileB))

That would be, under implicit async, explicit await:

let file_a = fs::read_async("fileA");
let file_b = fs::read_async("fileB");
let [file_a, file_b] = await!(Future::all([file_a, file_b]));
await!(process(fileA, fileB))

And under explicit async, implicit await:

let file_a = Future::of(async || { fs::read_async("fileA") });
let file_b = Future::of(async || { fs::read_async("fileB") });
let [file_a, file_b] = Future::all([file_a, file_b]).await();
process(file_a, file_b)

Did I get this right, @rpjohnst?

An interesting consequence of the second style is that, in an async context, you don’t care if you’re calling an async fn or a fn; both behave the same (modulo if you suspend or block to wait). If you want a deferred value (a Future), you construct one explicitly from a new async context. I used a constructor for Future here to be explicit, but Future::of(async || { ... }) would probably be spelled async! { ... }.

A Future then provides the async fn method Future::await(self) that then actually drives the Future in its caller’s context.

I never really understood the Kotlin style of async until now, but I think I do now. Based on the comparison I have above, I’m now up in the air on which style I’d prefer in a world where futures aren’t registered until they’re awaited.

Close but not quite. Here's what we'd actually see (simplifying by assuming a tokio::fs or something, and adjusting to match the existing APIs):

Fully qualified, forward-compatible syntax:

let file_a = async { fs::read("fileA") };
let file_b = async { fs::read("fileB") };
let files = await!(join_all(vec![file_a, file_b]));
await!(process(files))

Implicit async, explicit await (the main RFC):

let file_a = fs::read("fileA");
let file_b = fs::read("fileB");
let files = await!(join_all(vec![file_a, file_b]));
await!(process(files))

Explicit async, implicit await (this proposal, and the main difference I'm pointing out with your version):

let file_a = async { fs::read("fileA") };
let file_b = async { fs::read("fileB") };
let files = join_all(vec![file_a, file_b]);
process(files)

Aside from the cosmetic changes to make it a more accurate comparison, the third version has a couple of differences:

  • Future::of(async || { .. }) is overkill and is just an async block (as you mention).
  • join_all can itself be an async fn that merely constructs a JoinAll and calls .await() on it. You could leave it as join_all(vec![file_a, file_b]).await() if you want to keep the API the same as today's.

Both of these changes help enable the first version to be forward-compatible with the third version. The transition then becomes "leave async blocks as they are, and remove async! invocations." There's no need to add .await() calls as long as functions like join_all are async. (A transition to explicit await would go the other way- "remove (some) async blocks, leave async! invocations.")

5 Likes

Hello

I didn’t want to discuss my objections in the RFC, it seemed out of place. But let’s do it here, then.

I agree that the goal of async-await is ergonomics, therefore too much annotation is probably a problem. That can, however, be solved in other ways than pretending that something acts the same (in the syntax) while it doesn’t. One of the things I like about Rust is, it doesn’t paper over what happens. We have explicit heap allocation, for example, while other languages hide this. This makes me feel that I know what is happening in quite a detail and that I can influence it.

The specific points I don’t like about the proposal, and why:

First, I don’t buy the argument of normal function that can do arbitrary bad things. While it is true that in theory, it can ‒ I probably could write a function that moves the current stack into a different OS thread ‒ it is uncommon and you can guess it from the function name. If I have a function called load_from_file(filename), I can be quite confident it will not move me into another thread, that it’ll eventually return control to me and that it’ll not try to lock a mutex I haven’t provided to it. On the other hand, if I call scheduler::turn(), I can guess it just might do something interesting, so I’ll think twice about what state I’m in before the call. If the suspension points disappear, all functions are suspects for doing something bad and just reading the code (when doing code review, for example) is not enough to gain any level of reasonable confidence.

Let’s show an example (I’m not looking the exact interface of the sigprocmask binding into Rust, but it changes the masked signals for the current thread).

let old_mask = sigsetmask(SIG_BLOCK, SIGTERM | SIGQUIT)?; // Wait with the signal until we finish playing with the DB file
do_something_with_db()?;
sigsetmask(SIG_SETMASK, old_mask)?;

If the do_something_with_db is sync and I further assume that the author of the function was sane not to hide a very complex code to smuggle my stack into another thread (not a big assumption, I guess and if the author had bad intentions, he could crash my program in million other ways), this is OK. However, if it could mean a suspension point, it could set the signal mask for arbitrary other async task and I could restore the mask in a different thread, introducing a very hard to find bug. A hint at the suspension point (maybe just calling it with -()- instead of just ()) would be enough to both prevent this from slipping through code review and from compiling and introducing this bug by making that function async sometime in the future.

The same can be said for holding a mutex for a while. Let’s say I have single-threaded executor, but have other threads I want to sync with. If I hold it across a suspension point, I might get a deadlock, because other instance of the same code will run and will try to get the mutex too ‒ and can’t and it won’t let me run. Again, while in theory a function call could also try to lock the mutex, if it’s a library function to load metadata of a movie from file (something that could in theory be async), because I haven’t given it access to the mutex nor to my other tasks.

Both of these is something one might reasonably want to do in systems language like Rust.

Furthermore, the proposal doesn’t seem to look very consistent. OK, async functions have the ::new function and otherwise await when called directly. I don’t really see how that works, except adding a corner-case somewhere really deep into the language, while some kind of await! or operator is almost implementable as an extern crate. But what about integration with futures? Lot’s of library functions will return futures (TcpListener::accept, for example). Is that one awaited automatically? If so, who provides the ::new for it? Does it appear magically?

Furthermore, if it does auto-await in async function, but returns the future outside of it, then async is no longer an extention ‒ unsafe allows some more things to do inside of it, but the code that compiles outside does the same inside of it. This would be a code that does something else inside and outside. That looks confusing.

If you don’t auto-await the future, then you can only auto-await other async fns. Considering a lot of futures need to be implemented in other ways than an async fn, it’ll cover only small part of cases ‒ maybe 50%? Considering I’ll receive some futures by a parameter (and I’ll have to await them explicitly), some will be stored in some data structure, or receive by a channel, this’ll even lower the percentage of cases when it can be used.

Therefore, the result will be half implicit waiting and half explicit, getting disadvantages of both worlds.

5 Likes

The idea in “explicit async, implicit await” is that you wouldn’t (normally) be calling -> impl Future<T> functions. Instead, you call async -> T functions. That function can just construct and .await() the structure as in join_all above.

I think the fn::new part of the RFC here as written complicates things, and should just focus on async { fn } formulation of the concept.

The hurdle to both solutions is the namability of types for composition and storage of futures. When and where does stuff get pinned in our example, @rpjohnst?