Can an async function's result be returned without await'ing on it?

Is it possible to return the result from calling an async function without using .await on it?

For example, this code works:

async fn a() -> String {
    "test".to_owned()
}

async fn b() -> String {
    a().await
}

but it feels strange to have to put .await after calling a() given that b() has to return a future any way. If I remove this .await, the I get the error expected struct std::string::String, found opaque type, which makes sense as probably a() and b() return different Futures.

Is it possible to rewrite the above code snippet in such a way to improve performance?

If you want return the future without awaiting it, you can do this:

use std::future::Future;

async fn a() -> String {
    "test".to_owned()
}

fn b() -> impl Future<Output = String> {
    a()
}

However, you should not be doing this just for performance. Rust is not like other languages where every layer of future introduces allocations or anything like that.

7 Likes

It's not possible, because that result doesn't exist. Calling an async function doesn't create the string, it creates a state machine that will create a string when polled.

.await is actually running the code. If you don't run the code, then you can't have its result.

My question was about returning "something that returns a string". I do not want to evaluate the future before returning the result. I just want to return the future itself so that the caller decides what to do with it.

Unless you want "something that returns a something that returns a string", the code you've written should generate the optimal machine code.

The fact that it's an async function already means the caller can decide what to do with it.

(With the caveat that many types of futures must be either run to completion or dropped to satisfy high level constraints such as "don't leak memory".)

To summarize:

async
fn b ()
  -> String
{
    let ret: String = a().await;
    ret
}

is kind of the same as:

use ::futures::future::FutureExt;

fn b ()
  -> impl 'static + Future<Output = String>
{
    a()
    .map(|ret: String| ret)
}

And you are personally concerned about the performance implication of that dummy layer of .map(|ret: String| ret) wrapping; you'd like to do:

use ::futures::future::FutureExt;

fn b ()
  -> impl 'static + Future<Output = String>
{
    a()
-   .map(|ret: String| ret)
}

to "optimize" stuff.

The issue is, .map(|ret| ret) is just mapping the identity function, i.e., it is funneling a value through a no-op passthrough, which doesn't need any code to be added to feature the same semantics. In other words, the compiler is smart enough to figure out on its own and thus perform the optimization you mentioned, at least when using --release, which is the only time where this kind of super tiny micro-optimizations ought to matter. That's the whole point of a language featuring zero-cost abstractions :slightly_smiling_face:

It doesn't improve performance

This means that stripping that map / stripping the surrounding async … .await is not affecting performance in any way whatsoever.

It does hinder readability :warning:

Indeed,

async fn b() -> String

reads better than

fn b() -> impl 'static + Future<Output = String>

does it not? Especially when extra parameters are involved. Indeed, compare:

async
fn foo (a: &str, b: &str)
{
    bar(a, b).await
}

to:

fn foo<'fut, 'a, 'b> (a: &'a str, b: &'b str)
  -> impl 'fut + Future<Output = ()>
where
    &'a str : 'fut,
    &'b str : 'fut,
{
    bar(a, b)
}
4 Likes

I understand that within the current state of the language, the best overall solution is the one from my example.

Separate from this, I think the code could be simplified even further if something like this were to be allowed:

async fn a() -> String {
    "test".to_owned()
}

async fn b() -> String {
    a()
}

a() returns a future that returns a string, which is exactly the same as the result from b(). So when the caller of b() writes .await, it would be executed on the future returned by a().

I do not know Rust well enough to say whether the compiler is smart enough to do the above even though the language does not allow it. But, I do not understand why the language can't be extended in the future too support this.

Some people have suggested the idea of having "auto-.await" / implicit .await. While they never truly laid out the ground rules (because there has to be a way not to await on a future, in order to be able to send futures to other functions, such as the .boxed() adapter or any of the myriad of spawn(<future>) functions / methods out there), when discussing about this informally with some other people at the #async channel of the Community Server at Discord, we came up with a not so bad heuristic: the only thing implicitly .awaited would be a function call that yielded a(n impl) Future, when done within an async body:

// Given:
async
fn foo ()
  -> String
{
    "…".into()
}
// and
fn bar ()
  -> impl Future<Output = ()>
{
    println!("bar");
    async {
        println!("polled");
    }
}

// We could imagine writing:
async
fn example ()
{
    let _: String = foo();
    let () = bar();
    // same as `bar().boxed()` currently: prints "bar" right now
    let boxed = (|| bar().boxed())();
    // less noisy version with almost the same semantics, but
    // for the call to `bar()` being lazy: "bar" not yet printed.
    let boxed_lazy = async { bar() }.boxed();
    let spawn_handle = some_runtime::spawn(async move { foo() });
    let () = boxed.await; // prints "polled"
    let () = boxed_lazy.await; // prints "bar" and "polled"
    let _: String = spawn_handle.await;
}

Under this proposal, your snippet would indeed work:

async fn a() -> String {
    "test".to_owned()
}

async fn b() -> String {
    a() /* implicitly `.await`ed */
}

But until a fully fledged model of implicit .await were to be laid out, which managed to prove its usefulness, I doubt (and even maybe hope) we won't be getting that into the language, at least not by default: at the very least the whole behavior should be module-level opt-in, with then still the option to opt-out for some sub-module or function, since hiding the .awaits under the rug will hinder debugging for sure: no matter how hard the async world wants to make async fns and async { … } blocks look like linear control flow, the yield points and all the semantics they imply make the whole thing very much non-linear, so the programmer ought to be made aware of such things, imho, through visible .await / yield points.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.