Using async in type definitions

Dear magical Rust people,

I'm once again stuck with trying to cram an async function into something. This time it's a type, which will be used in an enum later. But for the minimal example, which doesn't work, I have this code. As you can see, I don't manage to define a function with this type, and calling it fails, too.

use futures::Future;

type IoFunc = Box<
    dyn Fn(String) -> dyn Future<Output = String>
>;

async fn convert(input: String) -> String {
    input
}

fn main()
{
    let f: IoFunc = Box::new(convert);
    println!("{:?}", f("one".into()));
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0271]: type mismatch resolving `<fn(String) -> impl futures::Future<Output = String> {convert} as FnOnce<(String,)>>::Output == (dyn futures::Future<Output = String> + 'static)`
  --> src/main.rs:13:21
   |
13 |     let f: IoFunc = Box::new(convert);
   |                     ^^^^^^^^^^^^^^^^^ expected trait object `dyn futures::Future`, found opaque type
   |
note: while checking the return type of the `async fn`
  --> src/main.rs:7:36
   |
7  | async fn convert(input: String) -> String {
   |                                    ^^^^^^ checked the `Output` of this `async fn`, found opaque type
   = note: expected trait object `(dyn futures::Future<Output = String> + 'static)`
               found opaque type `impl futures::Future<Output = String>`
   = note: required for the cast to the object type `dyn Fn(String) -> (dyn futures::Future<Output = String> + 'static)`

error[E0618]: expected function, found `Box<(dyn Fn(String) -> (dyn futures::Future<Output = String> + 'static) + 'static)>`
  --> src/main.rs:14:22
   |
13 |     let f: IoFunc = Box::new(convert);
   |         - `f` has type `Box<(dyn Fn(String) -> (dyn futures::Future<Output = String> + 'static) + 'static)>`
14 |     println!("{:?}", f("one".into()));
   |                      ^--------------
   |                      |
   |                      call expression requires function

Some errors have detailed explanations: E0271, E0618.
For more information about an error, try `rustc --explain E0271`.
error: could not compile `playground` due to 2 previous errors

Something to remember about trait objects (i.e. anything with a dyn) is that their actual size and type isn't known at runtime, so they always need to be behind a pointer of some sort.

The function itself is fine, because you've written Box<dyn Fn(...) -> ...>, however the return type says you are returning a dyn Future<...> by value. However, that's not possible because the compiler can't know how much space to set aside for the return value when it is stored on the stack.

I think you can fix this by returning Box<dyn Future<Output = String>>. There might also be something in the futures crate that takes an async function and returns a function which returns a boxed future.

2 Likes

Try using the BoxFuture alias from the futures crate instead of dyn Future.

1 Like

The return types of async fn are opaque, unnameable types. Thus the need to type-erase the return value, as you have attempted to do with -> dyn Future<Output = String>.

However, dyn Trait types are dynamically sized, and they have to be behind some kind of pointer, e.g. in a Box. The opaque types I mentioned aren't boxed, so you'll need some sort of wrapper function to wrap up and coerce the opaque return types into boxes.

That is, you need something like

type IoFunc = Box<
    dyn Fn(String) -> Box<dyn Future<Output = String>>
>;

which you could then construct like

    let _f: IoFunc = Box::new(|input| Box::new(convert(input)));

Playground.

A helper trait or function can help with conversions.


Left as an exercise: adjusting the examples to use BoxFuture.

2 Likes

Thank you very much for your explanation and example. To actually call the method I had to add an Unpin trait to the type, now it seems to work:

use futures::Future;

type IoFunc = Box<dyn Fn(String) -> Box<dyn Future<Output = String> + Unpin>>;

async fn convert(input: String) -> String {
    format!("{}/{}", input, input)
}

#[tokio::main]
async fn main() {
    let f: IoFunc = Box::new(|input| Box::new(Box::pin(convert(input))));
    println!("{}", f("something".into()).await)
}

BoxFuture uses lifetimes, which I avoided so far, so I don't know how to use it here :slight_smile:

Thanks for the suggestion, but I'm afraid of lifetimes for the moment...

You can put 'static as the lifetime in the type alias.

2 Likes

Very nice, that makes it a bit shorter:

use futures::future::BoxFuture;

type IoFunc = Box<dyn Fn(String) -> BoxFuture<'static, String>>;

async fn convert(input: String) -> String {
    format!("{}/{}", input, input)
}

#[tokio::main]
async fn main() {
    let f: IoFunc = Box::new(|input| Box::pin(convert(input)));
    println!("{}", f("something".into()).await)
}
1 Like

OK, now when I try to use it in my actual code, the method is in a structure, and then it fails:

use futures::future::BoxFuture;

type IoFunc = Box<dyn Fn(String) -> BoxFuture<'static, String>>;

struct Convert {
    a: String
}

impl Convert {
    async fn doit(&mut self, input: String) -> String {
        format!("{}/{}", self.a, input)
    }
}

#[tokio::main]
async fn main() {
    let conv = Box::leak(Box::new(Convert{a: "base".into()}));
    let f: IoFunc = Box::new(|input| Box::pin(conv.doit(input)));
    println!("{}", f("something".into()).await)
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/lib.rs:18:52
   |
18 |     let f: IoFunc = Box::new(|input| Box::pin(conv.doit(input)));
   |                                                    ^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'_` as defined here...
  --> src/lib.rs:18:30
   |
18 |     let f: IoFunc = Box::new(|input| Box::pin(conv.doit(input)));
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that closure can access `conv`
  --> src/lib.rs:18:47
   |
18 |     let f: IoFunc = Box::new(|input| Box::pin(conv.doit(input)));
   |                                               ^^^^
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the types are compatible
  --> src/lib.rs:18:38
   |
18 |     let f: IoFunc = Box::new(|input| Box::pin(conv.doit(input)));
   |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: expected `Pin<Box<(dyn futures::Future<Output = String> + std::marker::Send + 'static)>>`
              found `Pin<Box<dyn futures::Future<Output = String> + std::marker::Send>>`

For more information about this error, try `rustc --explain E0495`.
error: could not compile `playground` due to previous error

Well, the actual method in my structure uses the &mut self, unlike in this sandbox code. But it is necessary to make the error appear :slight_smile:

Do you need to call an IoFunc multiple times? If so, it isn't possible to make it work with &mut self. Every time you call f, that creates a new doit future, which borrows &mut conv. If multiple doit futures existed at the same time, that would create multiple &mut conv borrows, which isn't allowed. So a solution would require one of three things:

  1. Make IoFunc a FnOnce
  2. Use &self in doit, possibly with interior mutability
  3. Change IoFunc to take the &mut Convert reference every time it is called
2 Likes

OK - again, once it's explained, it makes sense :slight_smile: And I'm never sure whether my explanations are correct... So thanks a lot.

I need to call it multiple times. In my actual code I have another solution with using a trait, which works quite well. But I was hoping to make it nicer by using callbacks. Well, seems not so. Which is also a good result!