-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Do we need Send bounds to stabilize async_fn_in_trait? #103854
Comments
(NOT A CONTRIBUTION) I just read the blog post and I thought it would be a useful datapoint that I literally wrote code (using async-trait) that would have run into this issue earlier today. I spawn in generic code all the time; a common case is that there is some kind of dependency injection in order to mock code that would use the network for testing purposes; such code is an obvious use case for an async method (EDIT: and there are many other reasons to use generics for dependency injection, eg supporting multiple different transport protocols) |
The async code I write is rarely generic, rarely |
the unsend future return by async trait is unusually, in the past people use the async trait crate and they are familiar that the future is send, if not, will mark at the trait as an attribute, and people use tokio or async std runtime in most cases, both of them are work stealing runtime, which require the future is send |
I tried using this in axum a week or two ago. Because it does type erasure, it can only support either Edit: just saw the RPITIT mention in the blog post and I think that didn't work when I tried it, but I guess I should try again. |
(NOT A CONTRIBUTION)
First, committing to the current behaviour means not having methods be send-by-default (opposite of the behaviour of async trait); maybe the team has already reached consensus on this, but it is a foreclosure on an option which is currently working well for the only actually used implementation of async traits. So it is a decision that is being made, not merely punted. Even still, I don't think this is the only question at play here. The reality is that punted decisions have a tendency to stay punted for longer than initially expected. Async/await has already suffered reputational damage from how long its been a known-incomplete MVP and having cliffs like this very cliff. The team should consider what will be the effect of punting this issue. In particular, the Send issue will create a much more difficult cliff. Currently the limitation is "async methods are not allowed in traits," which sucks but is easy to understand and plan for. Without resolving this, you will be using async methods just fine until suddenly you need a Send bound and you can't. These methods could be coming from an external library you don't control; you may need to refactor all of your code or you may need to drop using that library if you don't have a solution for this issue. In my use case yesterday, the spawn was necessary to fix a bug in the code; if I couldn't've introduced it, I think we would have had to restructure our entire code base to fix that bug, because we used async methods before realising we would need to spawn in a generic context. This is not a trivial problem to run into. |
EDIT: this post was based on me minunderstanding async methods and was corrected by withoutboats below. (click to see the original post
In capnp-rpc we use single-threaded futures, and it looks like we run into the same problem, except with Here's a distillation of the problem: My impression is that we're going to need a way to specify bounds on the returned futures of async trait methods, like: trait MyServer {
async (: 'static) fn foo(&mut self) -> Result<(),String>;
async (: 'static) fn bar(&mut self) -> Result<(),String>;
} I remember at some point in the past (like a few years ago) someone suggested that |
(NOT A CONTRIBUTION) @dwrensha That doesn't work because those methods are not |
Ah, I see. I would have the same problem even if the more details (probably not relevant to the present issue)We use this pattern, where a method has ephemeral access to use std::future::Future;
use std::pin::Pin;
struct Foo {
x: String
}
trait FooServer {
fn foo(&self) -> Pin<Box<dyn Future<Output=()> + 'static>>;
}
impl FooServer for Foo {
fn foo(&self) -> Pin<Box<dyn Future<Output=()> + 'static>> {
// do stuff here that accesses &self.
println!("{}", self.x);
Box::pin(async {
// no longer have access to &self.
()
})
}
}
pub fn main() {
let foo = Foo { x: "hi".into() };
let fut : Pin<Box<dyn Future<Output=()> + 'static>> = foo.foo();
} It would be nice if we could make |
Not sure this is the right place to post this, but here are my opinions on...
|
A mechanism like @andrewgazelka outlines above for expressing bounds on what amount to anonymous associated types is an appealing general solution. Backwards-compatible syntax for such a feature is conceivable. Provided that something like that is possible, I think baking any particular set of bounds into the definition of async traits is a mistake that would be difficult to correct without breaking. Blocking features that are useful today because someday they'll hopefully be even better feels weird, although I sympathize that we don't want to introduce architectural traps. On the other hand, software is never complete, and I'm very glad that |
P.S. It would be even better if we can have something like |
An experience report: I'm fooling around with making a web framework similar to Compiler error message
(Also that "consider further restricting the associated type" suggestion obviously doesn't work.) Personally, I don't know if this is worth delaying stabilization since having |
For the associated type, when we want some constraints to that type, we may write fn show<U, T: Trait<Output = U>>(v:T) where U: ??? Similarly, we may design the trait bound for the associated function/method in a similar way, which looks like fn show<P0, P1, R, T: Trait< next(P0, P1 /*... Pn*/)->R >>(v:T) where P0: ???, P1:???, R: Send
{} which can have better fine-grained control over the corresponding types of the associated function/method. Furthermore, we can reuse the current syntax without creating any new syntax. fn show<P0:???, P1, R:Send, T: Trait< next(P0, P1 /* ... Pn */)->R >>(v:T)
{} For the method, I think fn show<P, R, T: Trait< next(/* & */T, P) -> R > >(v:T)
/* & mut */
{} might also be clear. |
I'm writing from a user's perspective who uses I suspect many people are just looking to be able to drop Generally I would expect the following after stabilization:
Without the ability to specify bounds the saying above will not hold true which is made worse by the fact that I think allowing RPITIT to specify bounds and lifetimes in trait definitions even if just restricted to I think specifying bounds of return types (e.g. with decltype syntax bikesheddingWhile I don't think /// All the examples mean the same, they restrict
/// the return type of the given functions.
trait Foo
where
typeof(Self::foo()): Send,
{
async fn foo(&self);
async fn bar(&self)
where
typeof(return): Send;
async fn baz(&self)
where
typeof(baz()): Send;
} |
More bikeshedTo bikeshed some more, I think removing /// All the examples mean the same, they restrict
/// the return type of the given functions.
trait Foo
where
typeof(Self::foo): Send,
{
async fn foo(&self);
async fn baz(&self)
where
typeof(baz): Send;
} |
Inspired by #103854 (comment). What if we can specify bounds of associated functions? #![feature(async_fn_in_trait)]
trait AsyncIterator {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
I::Item: Display,
I::next: for<'a> impl FnOnce(&'a mut I) -> impl Future<Output = Option<I::Item>> + Send + 'a
{
tokio::spawn(async move {
while let Some(x) = count.next().await {
println!("{x}");
}
});
} It seems too verbose. |
@Nugine Fn has associated type: Output, it may simplify to |
The syntax decltype(I::next): Trait and therefore we could also write the requirement as <decltype(I::next) as Fn<...>>::Output: Send pointed by @dojiong |
thoughts on
similar to discriminant but not requiring a value? Type would be compile generated to be equivalent to the return type |
(I do not work on the compiler, and I do not get to decide anything, the below text is only an opinion as an outside observer.) I can't help but see that this thread is turning into a decltype is difficult
This is just me playing around with the idea without all edge cases considered. I imagine properly designing and implementing this feature would push async traits way beyond the estimated 6 months in the async trait blog post. I do believe that
|
I agree with the bikeshedding. Honestly, I'd like to see |
For now, if you want to define an "async" trait, you may use:
We alreay have async_trait4 syntax widely used through proc macro. The macro can desugar to one of the five methods above when new features are stabilized. Only the last method has a "Send" problem. The solution is not trivial. So I think we should solve the "Send" problem before stabilizing async_fn_in_trait. Footnotes |
(NOT A CONTRIBUTION) I am not at liberty to post an example, perhaps someone else could come up with one. Structured concurrency isn't relevant here: even if Rust had chosen to push structured concurrency and had frameworks that don't provide spawn without a nursery you would still have the same problem: to make sure a task completes, you would need to pass a nursery into that context and spawn onto it. The nursery's spawn method would require Regardless, Rust didn't push structured concurrency and with the existing frameworks spawning is not an anti-pattern. |
(NOT A CONTRIBUTION) Another experience report: today I encountered an issue in which a minor refactor caused rustc's overly conservative analysis to believe a shared reference to a future returned by an async-trait async method had to be stored in an outer async function's future state, and therefore the future returned by that method needed to be async-trait doesn't provide a way to say that methods' return types must be This leads me toward the solution the team seems to favour - a syntax in where clauses for referring to the return type of an async method - which is flexible enough to support both And one other unrelated thought: |
But most of the time we should already know types of params in functions. And corresponding trait bounds should be imposed on type params of traits or functions instead of assigning a param variable and impose again. I think this is kind of repeating what you have already said (the types). If I'm understanding right, opaque returned futures are the only things we are interested in? |
I think making |
@tvallotton source/refs? what other runtimes do you prefer? this argument seems pretty subjective. I don't think anything can be "proven" to be easier to use... |
Well, I consider single threaded executors easier to use because the don't require me to place |
To explain myself a little bit more with respect to my opinions around work stealing. Suppose I wrote some function with |
Correct me if I'm wrong, but I don't think anyone was suggesting this. All the discussion I see is about how to require |
Oh yeah sorry, I was mostly commenting on this paragraph by @withoutboats, which I interpreted as proposing
|
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
To be fair to @tvallotton many |
Rarely do I need to spawn inside a generic function, let alone one that is generic over an async trait.
The mitigations seem sufficient, as long as compiler errors / clippy can lead users to the mitigations. I don't believe that Send should be a default requirement or require some special syntax to shorthand it. However, that being said, I do believe there is benefit to new people using the language to make it easier to make things work without over-complicating trait signatures. So I am not completely against the idea, as long as it makes sense and the related errors are helpful. |
The problem is that the mitigation, today, requires the ability change how the trait is written. If the trait comes from a different crate, you have no recourse.
The only way to solve the above problem is by adding new syntax. |
That is a problem with the design of that API. If the users of that API want Send bounds they should provide their input to the API designers, and they should use a mitigation. That should not be the concern of the language. In the end, even if the default was a Send bound and some simplified syntax to remove the bound was added, API designers could still get it wrong. |
Again, I don't think anyone is suggesting this.
Not being able to require ... Aside from using different language features (i.e. "a mitigation", e.g. rewrite the trait using TAIT, i.e. make a breaking change), but this doesn't solve the problem, it just avoids it. I don't think "use a mitigation when necessary" is practical because, with new syntax to support requiring
|
I want to expand on this: Having the ability to enforce the The use case for it is this: Ideally, axum(-core)'s traits would not require implementers of its |
(NOT A CONTRIBUTION)
This just isn't how Rust works. Trait bounds are composable so downstream users can impose multiple intersecting requirements on their types, including on the associated types of traits. E.G. users can write |
(NOT A CONTRIBUTION) A query: looking at the code sample for the RPIT "workaround," it looks like there's no way to use RPIT to have a single trait with an async method return type that could or could not be So if the feature were stabilised as is, the workaround would be to always provide a second version of the trait which requires So you can't have the blanket implementation. Instead you need something like Since library authors can't know how their libraries will be use, this would then be the best practice to make sure their library is useable by all their downstream users:
This workaround is a lot of additional complexity for users, and I think it will just exacerbate the impression that async Rust is full of bizarre pits to fall into as a user and be confronted with type nonsense you don't understand or care about. It sounds like the team already has consensus about the syntax they want, unless there's some huge problem with implementing return type bounding, I really think you should just do this correctly. EDIT: Actually, the workaround does not work without the blanket implementation. Without the blanket implementation, there's no guarantee that But the blanket implementation means that generic types will not be able to implement So the workaround is not just more boilerplate and a more confusing interface, but is actually not able to cover all the use cases Send bounds would cover. |
I have this usecase where in an MQTT library I want to support TCP, TLS and QUIC. I achieve this by using generics and async traits that should be send and sync to support the most used async runtimes. |
I think the proposed approach for writing something like fn spawn_print_all<I: AsyncIterator<next(): Send> + Send + 'static>(mut count: I)
// ^^^^^^^^^^^^^^ new (syntax TBD)
where
I::Item: Display,
{
...
} would actually make sense...
|
Idea: implied Send bound Async Rust already has an element of implicitness: A Future which holds a non-Send type across a yield point does not implement Could this idea be "abused" and taken one step further? trait AsyncTrait {
async fn foo(&self);
}
async fn foo<T: AsyncTrait>(t: T) {
tokio::spawn(async move {
t.foo().await;
});
}
The downside of course is that APIs may unexpectedly break by only changing implementation without also changing the signature. But the point as mentioned is that async Rust already behaves this way. The upsides are a smoother developer experience with reduced need for boilerplate generic bounds (given good error diagnostics), and avoid exposing new syntax. I'm not competent enough to judge whether this would be feasible to implement in the compiler. (the real signature of a function would depend on looking inside the function, maybe the closest analogy is edit: Just after I posted I realized this would not work in |
(NOT A CONTRIBUTION, JUST SUGGESTION) It's true that we need to figure out some syntax to declare extra bounds.. But I don't think this limitation should block the whole feature. The syntax of Maybe in the future we can invent a more powerful syntax to constraint extra bounds on method types outside the trait. I really love it. |
I agree. In my EventSourced library I am using "async fn in trait" in anger. I ran into the "future is not Send" issue, but it can be easily worked around as described in the blog post. I also ran into some other – probably related – issues like bogus higher-ranked lifetime error, but I also found workarounds. On the long run we need Send and other bounds and the whole developer experience should become much easier. But nevertheless the feature is already incredibly useful if one is willing to navigate around the existing issues. |
I wanted to refactor a part of our codebase and remove duplicated associated functions by making a trait with default methods and having the structs use that. Unfortunately, i eventually hit this problem. All of the functions in question are |
Thanks to everyone who left feedback! It seems like the answer to the question posed by this issue is "yes". I'm going to close this. The blog post talks about what's next: https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html |
Problem: Spawning from generics
Given an ordinary trait with
async fn
:It is not currently possible to write a function like this:
playground
Speaking more generally, it is impossible to write a function that
async fn
of the trait from the spawned taskThe problem is that the compiler does not know the concrete type of the future returned by
next
, or whether that future isSend
.Near-term mitigations
Spawning from concrete contexts
However, it is perfectly fine to spawn in a non-generic function that calls our generic function, e.g.
playground
This works because spawn occurs in a context where
Countdown
Countdown::next
isSend
print_all::<Countdown>
and passed tospawn
isSend
Making this work smoothly depends on auto trait leakage.
Adding bounds in the trait
Another workaround is to write a special version of our trait that is designed to be used in generic contexts:
playground
Here we've added the
Send
bound by using return_position_impl_trait_in_trait syntax. We've also addedSelf: Send + 'static
for convenience.For a trait only used in a specific application, you could add these bounds directly to that trait instead of creating two versions of the same trait.
For cases where you do need two versions of the trait, your options are
SpawnAsyncIterator
toAsyncIterator
(playground)Only work-stealing executors
Even though work-stealing executors are the most commonly used in Rust, there are a sizable number of users that use single-threaded or thread-per-core executors. They won't run into this problem, at least with
Send
.Aside: Possible solutions
Solutions are outside the scope of this issue, but they would probably involve the ability to write something like
Further discussion about the syntax or shape of this solution should happen on the async-fundamentals-initiative repo, or a future RFC.
Questions
async_fn_in_trait
until we have a solution?If you've had a chance to give
async_fn_in_trait
a spin, or can relay other relevant first-hand knowledge, please comment below with your experience.The text was updated successfully, but these errors were encountered: