Question about async fn in traits: return type is Send or not

//! https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits/

#![allow(dead_code)]

use std::marker::PhantomData;

fn assert_send<T: Send>(_: T) {}

trait Foo {
    // equal to `foo(&self) -> impl Future<..>`
    // not `impl Future<..> + Send`
    async fn foo(&self) {}
}

// A is Send
struct A(u8);

impl Foo for A {}

fn handle_a(a: A) {
    // no err!!!
    // ??? Why a.foo() is Send ???
    assert_send(a.foo());
}

// B is !Send
struct B(PhantomData<*mut u8>);

impl Foo for B {}

fn handle_b(b: B) {
    // err
    // b.foo() is not Send, correct
    assert_send(b.foo());
}

fn handle_foo<F>(f: F)
where
    F: Foo + Send,
{
    // err
    // although F is Send, f.foo() is not Send, correct
    assert_send(f.foo());
}

(Playground)

In handle_b, b.foo() is not Send, in handle_foo, f.foo() is not Send, this is expected. No matter b/f is Send or not, the future is not Send.

But in handle_a, a.foo() is suprisingly Send, this confuses me a lot. Compared A and B, I can assume that a.foo() is Send since A is Send. But f.foo() is not Send even f is Send, so the assumption is definitely wrong.

What is the key point here, what makes a.foo() Send?

async fn foo() is syntax sugar for fn foo() -> impl Future<...>. The trait Foo effectively has a hidden associated type for the future returned by foo(). For each impl Foo for T that actual associated type may or may not be Send.

As far as the trait is concerned, there is no necessary relationship between whether the implementing type (A or B) is Send and whether the associated future type is Send. In unstable RTN syntax, whether the bound T: Send holds has nothing to do with whether the bound T::foo(..): Send holds.

Thus, handle_foo() does not compile, because it doesn't have the bound where T::foo(..): Send, but handle_a() compiles, because the it is true that A::foo(..): Send — it can rely on this fact because handle_a is not generic. On nightly, this version of handle_foo compiles:

fn handle_foo<F>(f: F)
where
    F: Foo + Send,
    F::foo(..): Send,
{
    assert_send(f.foo());
}

In general, async fn in traits will be much more useful when RTN is stabilized.

2 Likes

Thanks a lot! Your answer and link of RTN RFC solves much of my confusion about async fn in trait!