Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 784
Nicolas Marshall Avatar asked Sep 30 '19 18:09

Nicolas Marshall


People also ask

Do async functions need to be called with await?

If you forget to use await while calling an async function, the function starts executing. This means that await is not required for executing the function. The async function will return a promise, which you can use later.

How are async functions executed?

An async function can contain an await expression, that pauses the execution of the function and waits for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value. You can think of a Promise in JavaScript as the equivalent of Java's Future or C# 's Task.

What happens by prefixing await to an async function call?

The await expression causes async function execution to pause until a promise is settled (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment. When resumed, the value of the await expression is that of the fulfilled promise.


2 Answers

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?
like image 131
Shepmaster Avatar answered Sep 26 '22 00:09

Shepmaster


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
like image 27
Ibraheem Ahmed Avatar answered Sep 27 '22 00:09

Ibraheem Ahmed