Lifetime issue with closure returning future

I've hit a lifetime issue with a test helper function. Maybe I'm trying to be too fancy, but it would still be helpful to understand what the issue is here.

/// A helper function for running test sequences that require both a client and 
/// device. This function will build the device & client, give the device time
/// to start up, then pass them to the provided closure to run the test.
/// The closure should return a future. The future will be run with a timeout
/// to ensure it doesn't hang forever. When the test is done the
/// client and device are cleanly shutdown.
pub async fn run<S,F>(seq: S)
where S: FnOnce(&mut Client, &mut Device)->F, 
      F: Future<Output=()> {
    info!("Creating device and client...");
    let mut device = device().await;
    let mut client = client().await;
    info!("Waiting 1s before starting test...");
    sleep(Duration::from_secs(1)).await;
    info!("Starting test...");
    info!("If test takes longer than 10s it will timeout.");
    let fut = seq(&mut client, &mut device);
    let result = timeout(Duration::from_secs(10), fut)
        .await;
    device.shutdown().await;
    //TODO: client shutdown.
    result.expect("Test timed out");
    info!("Test complete");
}

#[tokio::test]
async fn test_case() {
    let mut seq = |client, device| { 
        async {
            sleep(Duration::from_secs(1)).await;
        }
    };    
    run(seq).await;
}

Compile error

error[E0308]: mismatched types
  --> handover_tests.rs:14:5
   |
14 |     run(seq).await;
   |     ^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'a, 'b> FnOnce<(&'a mut wlink_client::Client, &'b mut wlink_device_lib::Device)>`
              found trait `FnOnce<(&mut wlink_client::Client, &mut wlink_device_lib::Device)>`
note: this closure does not fulfill the lifetime requirements
  --> handover_tests.rs:8:19
   |
8  |     let mut seq = |client, device| { 
   |                   ^^^^^^^^^^^^^^^^
note: the lifetime requirement is introduced here
  --> \lib.rs:28:10
   |
28 | where S: FnOnce(&mut Client, &mut Device)->F, 
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Reproducible Example: Rust Playground

Adding explicit types to the closure resolves the issue: Rust Playground

like so:

//                       vvvvvvvvvvvvv        vvvvvvvvvvvvv
    let mut seq = |client: &mut Client, device: &mut Device| async {
        sleep(Duration::from_secs(1)).await;
    };
1 Like

For reliable type inference, especially when higher-ranked lifetimes are involved, it’s generally best practice to pass closure expressions directly to functions that expect them:

    run(|client, device| { 
        async {
            sleep(Duration::from_secs(1)).await;
        }
    }).await;

even so, for the case of a function returning a future, you will very quickly encounter further problems, as soon as you try to use client or device inside of the async block.

To address this issue (assuming you don’t just need client or device before the async block), the best available current approach is to return a BoxFuture, which allows us to correctly specify the lifetime parameter of the future.

S: for<'a> FnOnce(&'a mut Client, &'a mut Device) -> BoxFuture<'a, ()>

With FutureExt in scope, the call-site can then look something like

    run(|client, device| { 
        async {
            // use `client` / `device`
        }
        .boxed()
    })
    .await;
2 Likes

perfect, thanks very much!