Rfcs: Support `async fn` in trait methods (async/await follow-up)

Created on 5 Aug 2019  Â·  7Comments  Â·  Source: rust-lang/rfcs

As noted by the author of the async/await RFC during its discussion:

If the answer is just "this can't be used on trait methods", that removes a lot of the utility of this feature, especially for libraries.

This RFC does not propose enabling async trait methods. I agree that trait methods are an extremely valuable use case for async notation, and from my perspective it is the absolute highest priority extension after this initial MVP proposal (in contrast to things like async generators).

https://github.com/rust-lang/rfcs/pull/2394#issuecomment-379704976

Now that the async/await MVP is tentatively approaching stabilization, this is the next logical feature that users are going to start asking about. Discussion of how to support this feature is currently fragmented across various other async/await issues threads, so I have opened this issue in order to centralize discussion (and hopefully to have someone in-the-know summarize the current obstacles and plans).

cc @withoutboats

A-async-await A-traits AsyncAwait-Triaged T-lang

Most helpful comment

The next step after existential associated types in traits might be to support -> impl Trait directly in traits via a desugaring to an associated type, something like that would seem necessary for async fn in traits as there is no way to link an existential associated type to an async fn in the implementation.

i.e. once GATs and "impl Trait in type aliases" are implemented it should be possible to do this:

trait Foo {
    type Bar<'a>: Future<Output = ()> + 'a;

    fn bar(&self) -> Self::Bar<'_>;
}

impl Foo for () {
    type Bar<'a> = impl Future<Output = ()> + 'a;

    fn bar(&self) -> Self::Bar<'_> {
        async move {
            println!("{:?}", self);
        }
    }
}

this could then be simplified to something like

trait Foo {
    fn bar(&self) -> impl Future<Output = ()> + '_;
}

impl Foo for () {
    fn bar(&self) -> impl Future<Output = ()> + '_ {
        async move {
            println!("{:?}", self);
        }
    }
}

and finally that should be fundamentally equivalent to

trait Foo {
    async fn bar(&self);
}

impl Foo for () {
    async fn bar(&self) {
        println!("{:?}", self);
    }
}

The biggest issues I see with the latter two sugars are how to deal with an unnameable associated type, e.g. first question I thought of is how do you use it as a trait object like you can with the first (type BoxDynFoo = Box<dyn Foo<Bar<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>>>)?

All 7 comments

I believe that this is blocked on GATs?

On Aug 5, 2019, at 10:57 AM, Ben Striegel notifications@github.com wrote:

As noted by the author of the async/await RFC during its discussion:

If the answer is just "this can't be used on trait methods", that removes a lot of the utility of this feature, especially for libraries.

This RFC does not propose enabling async trait methods. I agree that trait methods are an extremely valuable use case for async notation, and from my perspective it is the absolute highest priority extension after this initial MVP proposal (in contrast to things like async generators).

2394 (comment)

Now that the async/await MVP is tentatively approaching stabilization, this is the next logical feature that users are going to start asking about. Discussion of how to support this feature is currently fragmented across various other async/await issues threads, so I have opened this issue in order to centralize discussion (and hopefully to have someone in-the-know summarize the current obstacles and plans).

cc @withoutboats

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

After looking around I believe it is blocked on not only generic associated types but also existentials in traits, both of which have accepted RFCs (#1598 and #2071). The current metabug encompassing this work appears to be https://github.com/rust-lang/rust/issues/63066 , although I'm unclear what subset of those features are required for async trait functions specifically (we may very well need a full RFC to determine that).

The next step after existential associated types in traits might be to support -> impl Trait directly in traits via a desugaring to an associated type, something like that would seem necessary for async fn in traits as there is no way to link an existential associated type to an async fn in the implementation.

i.e. once GATs and "impl Trait in type aliases" are implemented it should be possible to do this:

trait Foo {
    type Bar<'a>: Future<Output = ()> + 'a;

    fn bar(&self) -> Self::Bar<'_>;
}

impl Foo for () {
    type Bar<'a> = impl Future<Output = ()> + 'a;

    fn bar(&self) -> Self::Bar<'_> {
        async move {
            println!("{:?}", self);
        }
    }
}

this could then be simplified to something like

trait Foo {
    fn bar(&self) -> impl Future<Output = ()> + '_;
}

impl Foo for () {
    fn bar(&self) -> impl Future<Output = ()> + '_ {
        async move {
            println!("{:?}", self);
        }
    }
}

and finally that should be fundamentally equivalent to

trait Foo {
    async fn bar(&self);
}

impl Foo for () {
    async fn bar(&self) {
        println!("{:?}", self);
    }
}

The biggest issues I see with the latter two sugars are how to deal with an unnameable associated type, e.g. first question I thought of is how do you use it as a trait object like you can with the first (type BoxDynFoo = Box<dyn Foo<Bar<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>>>)?

The biggest issues I see with the latter two sugars are how to deal with an unnameable associated type

If the question is how to refer to a type that's been implicitly generated in such a way, perhaps that means having something like type_of!(Foo::bar), or something akin to associated types scoped to specific methods e.g. Foo::bar::Future (with the rationale of "if traits themselves can have input types and output types, why can methods only have input types?").

(Alternatively perhaps we could paper over it somehow with something like dyn impl Future, if people can tolerate that. :P )

If you used the sugared notation for the trait would it be compatible with a non-async function returning a custom future type.

For example

trait Foo {
    async fn bar(&self);
}

impl Foo for () {
    fn bar(&self) -> impl Future<Output = ()> + '_ {
        MyFuture{...}
    }
}

You can always write an async fn that simply awaits the custom future type

trait Foo {
    async fn bar(&self);
}

impl Foo for () {
    async fn bar(&self) {
        MyFuture{...}.await
    }
}

There is a reason to want a non-async fn in the implementation though (whether you're returning a custom future type or not), sometimes you want to do some minimal synchronous setup before returning

impl Foo for () {
    fn bar(&self) -> impl Future<Output = ()> + '_ {
        // sync part of function
        async {
            // async part of function
        }
    }
}

Because the trait definition already constrains the lifetimes involved this is mostly useful as an optimization, you may be able to reduce the size of the futures state by pre-calculating some data from the arguments.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

p-avital picture p-avital  Â·  3Comments

rust-highfive picture rust-highfive  Â·  4Comments

steveklabnik picture steveklabnik  Â·  4Comments

yongqli picture yongqli  Â·  3Comments

torkleyy picture torkleyy  Â·  3Comments