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?
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.
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.
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".)
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
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
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:
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;
}
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.