Rust to stabilize `async fn` and return-position `impl Trait` in traits
blog.rust-lang.org
blog.rust-lang.org
I'm glad the Rust project is willing to ship useful but limited features quickly, see how people use them, and then iterate and slowly remove the restrictions in the future. I think it'll be more productive than taking another 3 years to solve all the remaining rough edges and problems.
That said, I’ve been using it seriously on nightly for a while already… and must say its ergonomics are off putting.
For example I maintain https://github.com/plabayo/tower-async (a fork of tower), and on its own it looks to work (except for the boxing and dynamic dispatching others have already discussed).
But once you throw it into the tokio ecosystem, building on top of something like hyper, and you suddenly are back into nightly territory due to having to specify trait bound for your async fn trait methods (eg: call(): Send).
It works, and you can see in my early WIP proxy repo (https://github.com/plabayo/rama). It’s not pretty but does work… that said it does put you in a nightly position once again due to having to specify Send/Sync trait bounds of trait async fn methods opaque futures…
In contrast the RFC that would allow me to write ‘type Future = impl Future<…’ seems to fit a lot better in the existing tokio ecosystem. Just my 2 cents. Anyway, I get its hard work and WIP, so congratulations again and thx for all the work!
(Edit: I do understand that the main source of my issues is due to the combination of writing very generic code and using multithreaded async (via tokio). Still, it’s not pretty. But perhaps it’s also because I still have a lot to learn on how to use and write this. The latter is my hope)
trait HttpService: Send {
fn fetch(&self, url: Url)
-> impl Future<Output = HtmlBody> + Send;
}
impl HttpService for MyService {
async fn fetch(&self, url: Url) -> HtmlBody {
// This works, as long as `do_fetch(): Send`!
self.client.do_fetch(url).await.into_body()
}
}Edit: also the problem is about the method async returned future trait bounds, not the Self.
``` #[trait_variant::make(HttpService: Send)] ... ```
In the given example, they create a trait with an async fn that returns HtmlBody, then the trait_variant macro makes a duplicate version of the trait but additionally with a Send bound on the future. Makes sense, because users commonly need the future to be Send.
But... what's the point of having two traits? Could there not just be one trait, with the Send bound? Doesn't the Send bound only increase the places it can be used? Are there scenarios where you need the future to not be Send?
trait Trait {
fn f(&self) -> futures::BoxFuture<...> {
let fut = async move { /* ... */ };
fut.boxed()
}
}
(which is essentially what `#[async_trait]` desugars to)I'm not sure how you would even support dynamic dispatch in the general case without implicitly boxing the return value, which would be deeply unpopular within the Rust community and ecosystem. If we're saying we're ok with implicit allocations for futures then the whole "zero cost abstraction" of async/await seems pointless.
Also note that "zero cost abstraction" means that "zero cost" over implementing it by hand there is no way to return an owned dyn sized type in current rust. Syntax that returns a box is inherently zero cost because there's no other way to spell it.
C++ `co_await` also forces an allocation for similar reasons and lets the optimizer get rid of them in the static case which predictably is a mixed bag in practice; at least in rust you can guarantee the static sized version doesn't allocate. That said I don't like forcing an implicit allocation it feels against Rust's ethos and the language will likely have to have better handling of dyn types writ large to get there.
trait T {
fn f(&self) -> impl U;
}
trait U {
fn g(&self);
}
fn h(t: &dyn T) {
// What type is u? how big is it?
let u = t.f();
// How does this method resolve?
u.g();
}
If `U` is object safe, and `f()` returns something that is `Sized` then it's possible to define a non-heap allocated `dyn U` that is essentially a tuple `(<method table>, <anonymous>)` and this code would desugar to something like fn h(t: &dyn T) {
let u: (<method table>, <anonymous>) = t.f();
u.0.g(&u.1);
}
But that kind of requires a special calling convention for `T::f` when invoked from a `dyn T` that also returns the size of its return type.edit: Thinking about it some more, what could be done is that functions called from a `dyn Trait` object that are `impl Trait` have a special calling convention where they return everything on the stack. Then callsites take the top of the stack + offset of the method as the function arg and stack + sizeof the method table as their `self` argument and everything else is fine.
I'm not sure if LLVM supports this directly. I know some stack based language implementations and LISPs will do this to avoid writing register allocators so everything just gets dumped on the stack.
Also there's no way (afaik) in Rust to "spell" a non-heap allocated dyn Trait object. There's bare `dyn Trait` but that means something else and is preserved for backwards compatibility, iirc.
&dyn Trait is fine.
let i: i32 = 5;
let d = &i as &dyn Display;is it trivial to replace every use of the #[async_trait] with this, once it releases? async_trait was just a macro for adding a bunch of `Box<Pin<dyn Future<Output = BlahBlah> + OtherJunk>` and so on and so on.
Is async_trait entirely obsolete now, or are there still situations where async_trait must be used, where builtin async will not?
edit: if I read one more paragraph I would have seen the answer, it's right there in the post :)
Cfr: https://doc.rust-lang.org/1.8.0/book/trait-objects.html
This in contrast to generics which is static.
Cfr: https://github.com/plabayo/tower-async/pull/12
So far didn’t manage to produce a box that works without everything else in my tower async stack having to need send sync trait bounds as well… and of course needing nightly ‘call(): Send’ return bounds.
Despite that, haven’t found a reasonable working solution to support that in ‘tower-async’
Might be just me still having to b learn a lot. More then happy to hear feedbacks add help on these matters.
And of course this is a WIP. So fully understand. Respect to the entire async wg team. Onwards and forward.
If you need to use something like `Box<dyn MyTrait>`, then you'll want to continue using `#[async_trait]`.
Are there some cases, say with really big futures, where removing the boxing actually hurts performance?
The future we're working toward is for this to be a decision you can make locally without introducing compatibility hazards or having to change a bunch of code. Ideally, one day you can even ask the compiler to alert you to potential performance hazards or make reasonable default choices for you...