Type annotations while storing an async function inside a struct

I am having a bit of trouble storing a future generated from the async function work inside MyStruct

use std::future::Future;

struct MyStruct<T: Future> {
    task: fn() -> T,
}

async fn work() {}

The following code works:

fn first_test() {
    let y = MyStruct { task: work };
}

but this fails:

impl<T: Future> MyStruct<T> {
    fn new() -> Self {
        // mismatched types expected fn pointer `fn() -> T` found fn item `fn() -> impl Future {work}`
        Self { task: work } 
    }
}

fn second_test() {
    // type annotations needed for `MyStruct<T>`
    let x = MyStruct::new();
}

How can I add type annotations so that the initialization approach in second_test works?

I get similar errors when I try to do this with a closure too:

struct AnotherStruct<C: Fn() -> T, T: Future> {
    task: C,
}

impl<C: Fn() -> T, T: Future> AnotherStruct<C, T> {
    fn new() -> Self {
        // mismatched types expected type parameter `C` found fn item `fn() -> impl Future {work}
        Self { task: work }
    }
}

fn third_test() {
    // type annotations needed for `AnotherStruct<C, T>` cannot satisfy `<_ as FnOnce<()>>::Output == _`
    let w = AnotherStruct::new();
}

Where does work come from? The compiler needs to infer the type, so you need to pass an actual value at some point. This works:

impl<C: Fn() -> T, T: Future> AnotherStruct<C, T> {
    fn new(work: C) -> Self {
        Self { task: work }
    }
}

fn third_test() {
    let w = AnotherStruct::new(|| async { 42 });
}

You can't solve this with a type annotation because async functions have anonymous types that cannot be written directly.

1 Like

While the code you provided compiles, I was trying to use work from the global scope as defined in my first code block. This is still failing to compile:

async fn work() {}

impl<C: Fn() -> T, T: Future> AnotherStruct<C, T> {
    fn new() -> Self {
        // perhaps the compiler needs a hint here?
        let work: C = work;
        Self { task: work }
    }
}

fn third_test() {
    let w = AnotherStruct::new();
}

The issue is that you're asking the user for a type T and then promising to return a future of that type T. But work() only returns a single type of future, which isn't necessarily of the type T that the user asked for. One way around this problem is something like this:

use std::future::Future;

struct MyStruct<T> {
    task: fn() -> T,
}

async fn work() {}

impl MyStruct<()> {
    fn new() -> MyStruct<impl Future<Output=()>> {
        MyStruct { task: work } 
    }
}

fn second_test() {
    let x = MyStruct::new();
}

Playground

3 Likes

@2e71828, is the impl MyStruct<()> a typo or do I need to do some more reading? I guess the latter since your code indeed works. I would have thought the implementation block would have been something like:

impl MyStruct<impl Future<Output=()>> {
    fn new() -> MyStruct<impl Future<Output=()>> {
        MyStruct { task: work } 
    }
}

impl MyStruct<()> is valid and can sometimes be used to avoid type inference errors. It means you provide an implementation for a concrete value of generic argument: MyStruct::new will only be available for MyStruct<()>, not any MyStruct<T>. However, I don't see how this approach would be useful here.

1 Like

Because the returned Future is an unnameable type, I needed a nameable placeholder type for the impl block to make the type inference work. You can use any type you like here, as no MyStruct<()> will ever be created.

In effect, I'm using it to define some free functions in the MyStruct namespace. It's roughly equivalent to this:

fn new_work() -> MyStruct<impl Future<Output=()>> {
    MyStruct { task: work } 
}

fn second_test() {
    let x = new_work();
}
3 Likes

Or box all of it:

use std::future::Future;
use futures::future::BoxFuture;

struct MyStruct<T> {
    task: Box<dyn Fn() -> BoxFuture<'static, T>>,
}

impl<T> MyStruct<T> {
    fn new<F>(work: fn() -> F) -> Self where F: Future<Output=T> + Send + Sync + 'static {
        Self { task: Box::new(move || Box::pin(work())) } 
    }
}

For sure, one's life is easiest if you put everything behind a box, but I want to understand how to solve this problem without having to rely on allocation.

I think I am starting to understand the crux of problem now. Taking the following code into consideration:

use std::future::Future;

struct AnotherStruct<C: Fn() -> T, T: Future<Output = i32>> {
    task: C,
}

impl<C: Fn() -> T, T: Future<Output = i32>> AnotherStruct<C, T> {
    fn new() -> Self {
        Self { task: || async { 42 } }
    }
}

fn main() {
    let w = AnotherStruct::new();
}

The issue here is that new is a generic implementation block which is supposed to be generic over T and C. However my new function is the opposite, it only works for the specific closure C and future T created by the closure and async block.

So in your solution, @2e71828, MyStruct::new() is basically shorthand for MyStruct::<()>::new()

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.