How can I store an async function in a struct and call it from a struct instance?
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
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?