Specify whether Futures returned by async fn's are Send

Continuing the discussion from We hate the async fn ;-; (/lh):

I was wondering if I can clarify that my function returns a future that is Send. This is what I came up with:

pub mod m {
    async fn nop() {
    }
    pub fn foo() -> impl std::future::Future<Output = ()> + Send {
        async move {
            // When we later add this, we get a compiler error:
            //struct NotSend(std::marker::PhantomData<*const ()>);
            //let _not_send = NotSend(std::marker::PhantomData);
            nop().await;
        }
    }
}

(Playground)

But things get more complex when the question whether the future is Send or !Send depends on a type argument. Consider this:

pub mod m {
    async fn nop() {
    }
    pub async fn foo<T>(value: T) {
        // We later add some more code to this function without changing its signature:
        //struct NotSend(std::marker::PhantomData<*const ()>);
        //let _not_send = NotSend(std::marker::PhantomData);
        nop().await;
        drop(value);
    }
}

pub fn run() -> impl std::future::Future<Output = ()> + Send {
    m::foo(())
}

(Playground)

Here foo (in its current implementation) returns a Sendable future if T is Send. But I guess there is no way to express it in Rust? So when foo's implementation is changed, the module m compiles properly, but the function run won't compile. :slightly_frowning_face: Also rustdoc doesn't seem to document whether a returned future is Send (or when it is Send).

When I do this:

pub mod m {
    async fn nop() {
    }
    pub fn foo<T>(value: T) -> impl std::future::Future<Output = ()> + Send
    where
        T: Send,
    {
        async move {
            //struct NotSend(std::marker::PhantomData<*const ()>);
            //let _not_send = NotSend(std::marker::PhantomData);
            nop().await;
            drop(value);
        }
    }
}

pub fn run() -> impl std::future::Future<Output = ()> + Send {
    m::foo(())
}

(Playground)

Then I have to restrict T to always be Send, which will cause a chain-reaction of required trait-bounds in my code, having to add Send everywhere.

Is there any nicer solution to this problem and/or what is the current state of discussion? I assume this issue also comes up when implementing async traits?

Well, I just replied over there. As for the extended generic question...

  • If you want it restricted, test or desugar like you have done.
  • If you don't want it restricted, don't desugar.

I don't know if there's any discussion about a nicer way, but having to chain bounds everywhere isn't async or even RPIT specific. Implied bounds might help some but I don't have a good feel for how far implied bounds will try to take things (or when it might happen -- I feel the inference breakage will be great without further complications).

The more general "does the return implement an auto-trait" issue is also not async/RPIT specific. Does Vec::new return something that's Send?

Yeah, I found this link enlightening:

It says:

  • Low real change, since the situation already somewhat exists on structs with private fields:

    • In both cases, a change to the private implementation might change whether a OIBIT is implemented or not.
    • In both cases, the existence of OIBIT impls is not visible without documentation tools
    • In both cases, you can only assert the existence of OIBIT impls by adding explicit trait bounds either to the API or to the crate's test suite.

I always thought it's a particular problem of async fn, but it's not.

Desurgaring only works when the Send bound doesn't depend on a type argument. So this can't be solved with desugaring: pub async fn foo<T>(value: T), which is Send if and only if T is Send. I guess this may be a problem for async traits also in the future.

I will resort to adding tests then to ensure that certain methods are Send if this is relevant, and possibly add a guarantee to the documentation. Or ignore the problem and hope nothing bad happens. :grin:


  1. OIBIT is another term for auto-trait ↩︎

I don't know of any better solution than a file like this.

1 Like

There's no way to guarantee the if-and-only-if with either the sugared or desugared forms, right. And that's also true of structs: You can't look at the header and know. It depends on the fields (recursively) and on implementations.

Constructor T: Send Constructor<T>: Send Notes
Vec<_> :white_check_mark: :white_check_mark:
Vec<_> :x: :x:
*mut _ :white_check_mark: :x: Or &Cell<_>
*mut _ :x: :x:
&_ :white_check_mark: :question: iff T: Sync
&_ :x: :question: iff T: Sync
&Exclusive<_> :white_check_mark: :white_check_mark: Or &DynMetadata<_>
&Exclusive<_> :x: :white_check_mark:
2 Likes