How can I store an async function in a struct and call it from a struct instance?

I'm trying to achieve this with the new async/await syntax, std::future::Futures and a recent version of Tokio. I'm using Tokio 0.2.0-alpha.4 and Rust 1.39.0-nightly.

Different things I've tried include:

  • using Box<dyn>s for the types that I want to store in the struct
  • using generics in the struct definition

I couldn't quite get a minimal working version, so here's a simplified version of what I'm trying to achieve:

async fn foo(x: u8) -> u8 {
    2 * x
}

// type StorableAsyncFn = Fn(u8) -> dyn Future<Output = u8>;

struct S {
    f: StorableAsyncFn,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let s = S { f: foo };

    let out = (s.f)(1).await;

    Ok(())
}

Of course this code fails to compile with the following error:

error[E0412]: cannot find type `StorableAsyncFn` in this scope

StorableAsyncFn is not defined here, it's the type I'm trying to define.


Solution 1:

Let's use this as our Minimal, Reproducible Example:

async fn foo(x: u8) -> u8 {
    2 * x
}

struct S {
    foo: (),
}

async fn example() {
    let s = S { foo };
}

It produces the error:

error[E0308]: mismatched types
  --> src/main.rs:10:17
   |
10 |     let s = S { foo };
   |                 ^^^ expected (), found fn item
   |
   = note: expected type `()`
              found type `fn(u8) -> impl std::future::Future {foo}`

The type of foo is a function pointer that takes a u8 and returns some type implementing the trait std::future::Future. async fn is effectively just syntax sugar that transforms -> Foo into -> impl Future<Output = Foo>.

We make our struct generic and place a trait bound on the generic that matches. In real code, you'd probably want to place a constraint on the the Output associated type, but it's not needed for this example. We can then call the function like any other callable member field:

async fn foo(x: u8) -> u8 {
    2 * x
}

struct S<F>
where
    F: std::future::Future,
{
    foo: fn(u8) -> F,
}

impl<F> S<F>
where
    F: std::future::Future,
{
    async fn do_thing(self) {
        (self.foo)(42).await;
    }
}

async fn example() {
    let s = S { foo };
    s.do_thing().await;
}

To be even more flexible, you could use another generic to store a closure, instead of forcing only a function pointer:

struct S<C, F>
where
    C: Fn(u8) -> F,
    F: std::future::Future,
{
    foo: C,
}

impl<C, F> S<C, F>
where
    C: Fn(u8) -> F,
    F: std::future::Future,
{
    async fn do_thing(self) {
        (self.foo)(42).await;
    }
}

See also:

  • How do I call a function through a member variable?
  • How do I store a closure in a struct in Rust?

Solution 2:

Another way to store an async function is with trait objects. This is useful if you want to be able to swap out the function dynamically at runtime, or store a collection of async functions. To do this, we can store a boxed Fn that returns a boxed Future:

use futures::future::BoxFuture; // Pin<Box<dyn Future<Output = T> + Send>>

struct S {
    foo: Box<dyn Fn(u8) -> BoxFuture<'static, u8>,
}

However, if we try to initialize S, we immediately run into a problem:

async fn foo(x: u8) -> u8 {
    x * 2
}

let s = S { foo: Box::new(foo) };
error[E0271]: type mismatch resolving `<fn(u8) -> impl futures::Future {foo} as FnOnce<(u8,)>>::Output == Pin<Box<(dyn futures::Future<Output = u8> + 'static)>>`
  --> src/lib.rs:14:22
   |
5  | async fn foo(x: u8) -> u8 {
   |                        -- the `Output` of this `async fn`'s found opaque type
...
14 |     let s = S { foo: Box::new(foo) };
   |                      ^^^^^^^^^^^^^ expected struct `Pin`, found opaque type
   |
   = note: expected struct `Pin<Box<(dyn futures::Future<Output = u8> + 'static)>>`
           found opaque type `impl futures::Future`

The error message is pretty clear. S expects a owned Future, but async functions return impl Future. We need to update our function signature to match the stored trait object:

fn foo(x: u8) -> BoxFuture<'static, u8> {
    Box::pin(async { x * 2 })
}

That works, but it would be a pain to Box::pin in every single function we want to store. And what if we want to expose this to users?

We can abstract the boxing away by wrapping the function in a closure:

async fn foo(x: u8) -> u8 {
    x * 2
}

let s = S { foo: Box::new(move |x| Box::pin(foo(x))) };
(s.foo)(12).await // => 24

This works fine, but we can make it even nicer by writing a custom trait and performing the conversion automagically:

trait AsyncFn {
    fn call(&self, args: u8) -> BoxFuture<'static, u8>;
}

And implement it for the function type we want to store:

impl<T, F> AsyncFn for T
where
    T: Fn(u8) -> F,
    F: Future<Output = u8> + 'static,
{
    fn call(&self, args: u8) -> BoxFuture<'static, u8> {
        Box::pin(self(args))
    }
}

Now we can store a trait object of our custom trait!

struct S {
    foo: Box<dyn AsyncFn>,
}


let s = S { foo: Box::new(foo) };
s.foo.call(12).await // => 24