async-trait icon indicating copy to clipboard operation
async-trait copied to clipboard

Problem with implementing trait for `async fn`s

Open WaffleLapkin opened this issue 4 years ago • 5 comments

I'm trying to implement trait with async fn for all async fns. Simplified example:

#[async_trait]
trait Trait {
    async fn run(&self);
}

#[async_trait]
impl<F, Fut> Trait for F
where
    F: Fn() -> Fut + Sync,
    Fut: Future<Output = ()> + Send,
{
    async fn run(&self) {
        self().await
    }
}
simplified `cargo expand`
trait Trait {
    fn run<'s, 'async_trait>(&'s self) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>
    where
        's: 'async_trait,
        Self: 'async_trait;
}

impl<F, Fut> Trait for F
where
    F: Fn() -> Fut + Sync,
    Fut: Future<Output = ()> + Send,
{
    fn run<'s, 'async_trait>(&'s self) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>
    where
        's: 'async_trait,
        Self: 'async_trait,
    {
        #[allow(clippy::used_underscore_binding)]
        async fn __run<F, Fut>(_self: &F)
        where
            F: Fn() -> Fut + Sync,
            Fut: Future<Output = ()> + Send,
        {
            _self().await
        }
        Box::pin(__run::<F, Fut>(self))
    }
}

But sadly, this doesn't work:

error[E0309]: the parameter type `Fut` may not live long enough
  --> src/lib.rs:16:1
   |
16 | #[async_trait]
   | ^^^^^^^^^^^^^^
17 | impl<F, Fut> Trait for F
   |         --- help: consider adding an explicit lifetime bound `Fut: 'async_trait`...
   |
note: ...so that the type `impl std::future::Future` will meet its required lifetime bounds
  --> src/lib.rs:16:1
   |
16 | #[async_trait]
   | ^^^^^^^^^^^^^^

error: aborting due to previous error

(adding Fut: 'async_trait is impossible because it leads to impl has stricter requirements than trait errors)

But with by-hand desugaring this implementation is possible:

impl<F, Fut> Trait for F
where
    F: Fn() -> Fut + Sync,
    Fut: Future<Output = ()> + Send,
{
    fn run<'s, 'async_trait>(&'s self) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>
    where
        's: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move { self().await })
    }
}

#[test]
fn test() {
    let closure = || async { () };
    Trait::run(&closure);
}

So, my questions are:

  1. Why the first implementation doesn't work, but the second does?
  2. Is it possible to remove async move {} from the last example? (Box::pin(self()) leads to the same error the parameter type Fut may not live long enough)
  3. Is it possible to write implementation like this, but without so much boilerplate?
  4. Can async_trait to accept implementations like in the first example? (after some changes in how macro works)

WaffleLapkin avatar Nov 02 '19 11:11 WaffleLapkin

This is a compiler limitation with how Output associated types of Fn-family traits are specified. One way to work around this it is to avoid Fn by using an equivalent blanket implemented trait that avoids the special Fn trait syntax.

use async_trait::async_trait;
use std::future::Future;

trait AnyFn {
    type Output;
    fn call(&self) -> Self::Output;
}

impl<F, T> AnyFn for F
where
    F: Fn() -> T,
{
    type Output = T;
    fn call(&self) -> Self::Output {
        self()
    }
}

#[async_trait]
trait Trait {
    async fn run(&self);
}

#[async_trait]
impl<F> Trait for F
where
    F: AnyFn + Sync,
    F::Output: Future<Output = ()> + Send,
{
    async fn run(&self) {
        self.call().await
    }
}

dtolnay avatar Nov 02 '19 17:11 dtolnay

Thanks!

It's also worth noticing that this can be done throw #![feature(unboxed_closures)] and Fn<()>:

#![feature(unboxed_closures)]

#[async_trait]
trait Trait {
    async fn run(&self);
}

#[async_trait]
impl<F> Trait for F
where
    F: Fn<()> + Sync,
    F::Output: Future<Output = ()> + Send,
{
    async fn run(&self) {
        self().await
    }
}

WaffleLapkin avatar Nov 02 '19 21:11 WaffleLapkin

I've just found out that in my code this doesn't work :(

Because

  1. I have param in both Trait and F
  2. I have a struct that also implements Trait

So my code produce the following error:

error[E0119]: conflicting implementations of trait `Trait<_>` for type `Run`:
  --> src/lib.rs:34:1
   |
22 | / impl<A, F> Trait<A> for F
23 | | where
24 | |     F: StableFn<A>,
25 | |     F::Output: Future<Output = ()>,
...  |
29 | |     }
30 | | }
   | |_- first implementation here
...
34 |   impl<A> Trait<A> for Run {
   |   ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Run`
   |
   = note: downstream crates may implement trait `StableFn<_>` for type `Run`

example on playground

However, unboxed Fn trait works (example on playground)

See also https://github.com/rust-lang/rust/issues/48869 and https://github.com/rust-lang/rust/issues/50238

WaffleLapkin avatar Nov 02 '19 22:11 WaffleLapkin

= note: downstream crates may implement trait `StableFn<_>` for type `Run`

That's so crazy. :(

dtolnay avatar Nov 02 '19 22:11 dtolnay

I'm having trouble making this solution work when using a borrowed argument as with:

[#async_trait]
trait Hello {
  async fn say_hi(&self, to_whom: &'_ str);
}

It seems no matter what I try I run into lifetime issues associated with to_whom that I can't satisfy. This appears to be due to the fact that I'm needing to specify a constraint that to_whom must be bound to lifetime of the the returned Future but there's no syntax I can find to achieve this. See my playground attempt here:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e77117e78782e90097105a9a943188f7

This rustc issue I filed may also be relevant: https://github.com/rust-lang/rust/issues/95182 as this is what's required to make it work without traits despite bizarre rustc error messages.

jasta avatar Mar 24 '22 20:03 jasta

Closing as this is not going to be actionable in async-trait.

dtolnay avatar Nov 26 '22 20:11 dtolnay

I'm having trouble making this solution work when using a borrowed argument as with:

[#async_trait]
trait Hello {
  async fn say_hi(&self, to_whom: &'_ str);
}

@jasta, it's more than a year later but I was also having trouble with this. It seems like it's possible if you make the AnyFn trait generic over the lifetime so you can ensure the lifetime of the input parameter and the lifetime of the resulting future the same.

Playground link of your code modified to compile.

samvrlewis avatar Sep 09 '23 04:09 samvrlewis