Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementation of FnOnce is not general enough in async fn

Tags:

rust

The following code (playground) fails to compile:

async fn wrapper<F, Fut>(func: F)
where
    F: FnOnce(&i32) -> Fut,
    Fut: Future<Output = ()>,
{
    let i = 5;
    func(&i).await;
}

async fn myfunc(_: &i32) {}

fn main() {
    wrapper(myfunc);
}

The error message is:

error: implementation of `std::ops::FnOnce` is not general enough
   --> src/main.rs:15:5
    |
15  |       wrapper(myfunc);
    |       ^^^^^^^ implementation of `std::ops::FnOnce` is not general enough
    |
    = note: `std::ops::FnOnce<(&'0 i32,)>` would have to be implemented for the type `for<'_> fn(&i32) -> impl std::future::Future {myfunc}`, for some specific lifetime `'0`...
    = note: ...but `std::ops::FnOnce<(&i32,)>` is actually implemented for the type `for<'_> fn(&i32) -> impl std::future::Future {myfunc}`

I have found a few similar items that mention Higher-Ranked Trait bounds (e.g. this Rust issue and this one), and tried experimenting with for<'a> and additional lifetime bounds, but the error is still not clear to me.

I don't understand why the compiler can't determine the lifetimes, especially since a simpler version without futures compiles just fine:

fn myfunc(_: &i32) {}

fn wrapper<F: FnOnce(&i32)>(func: F) {
    let i = 5;
    func(&i);
}

fn main() {
    wrapper(myfunc);
}

What is the actual problem here? Is there a workaround that can make this compile, e.g. with additional bounds?

like image 463
gavrie Avatar asked Oct 12 '20 15:10

gavrie


Video Answer


1 Answers

Starting from what is explained in the issue you provided, a specific trait can be created for the functions your wrapper expects.

Being myself quite new to Rust, I don't know exactly why in your original version the compiler complains about mismatching types that are actually equivalent in the displayed error message (it is even more obvious when we had explicit lifetimes ; the displayed types match perfectly!). I simply guess this is due to the fact that the exact result type is hidden behind an impl notation and the actual difference lies in this hidden part.

It is indeed difficult to express that any lifetime for<'r> should be considered for the & parameter as well as for the Future result because they appear on two different constraints of the where clause. As far I understand (not so much...), considering an explicit lifetime for the whole trait makes the for<'r> notation consistent for the & parameter as well as the Future result because all of this is in the same constraint of the where clause.

Since we are dealing with lifetimes, I decided to replace the copyable i32 by a non-copyable String in order to prevent any unexpected simplifications from happening.

As stated in the link you gave, this solution seems to work only with functions, not closures.

use std::future::Future;

async fn wrapper<F>(func: F)
where
    F: for<'r> Wrapped<'r>,
{
    let s = String::from("WRAPPED");
    func.call_once(&s).await;
}

trait Wrapped<'a> {
    type Res: Future<Output = ()>;
    fn call_once(
        self,
        s: &'a String,
    ) -> Self::Res;
}

impl<'a, F, FutRes> Wrapped<'a> for F
where
    F: FnOnce(&'a String) -> FutRes,
    FutRes: Future<Output = ()> + 'a,
{
    type Res = FutRes;
    fn call_once(
        self,
        s: &'a String,
    ) -> Self::Res {
        self(s)
    }
}

async fn call_myfunc() {
    let s = String::from("HARDCODED");
    myfunc(&s).await;
}

async fn myfunc(arg: &String) {
    println!("arg={}", arg);
}

fn main() {
    println!("~~~~ hardcoded call ~~~~");
    let f = call_myfunc();
    futures::executor::block_on(f);
    println!("~~~~ use wrapper() ~~~~");
    let f = wrapper(myfunc);
    futures::executor::block_on(f);
}
like image 86
prog-fh Avatar answered Sep 28 '22 15:09

prog-fh