Noob needs help with an async closure problem ;)

Hello,

I'm diving into Rust by following along a rather "hands on" book, going great so far.

I'm going a little off the beaten track though, and facing a problem where I would like to improve the following (working) test code:

async fn init_stuff() -> u8 {
    println!("INIT STUFF");
    1
}

async fn fetch() -> u8 {
    println!("FETCH DATA");
    2
}

async fn cleanup() {
    println!("CLEANUP");
}

#[tokio::test]
async fn testing() {
    let conn_id = init_stuff().await;

    println!("--> conn_id={}", conn_id);
    assert_eq!(1, conn_id);

    let res = fetch().await;
    assert_eq!(2, res);

    cleanup().await;
}

By wrapping the main testing logic this way:

async fn init_stuff() -> u8 {
    println!("INIT STUFF");
    1
}

async fn fetch() -> u8 {
    println!("FETCH DATA");
    2
}

async fn cleanup() {
    println!("CLEANUP");
}

async fn within_transaction(x: fn(u8)) {
    let conn_id = init_stuff().await;
    x(conn_id);
    cleanup().await;
}

#[tokio::test]
async fn testing() {
    within_transaction(|conn_id| {
        println!("--> conn_id={}", conn_id);
        assert_eq!(1, conn_id);

        let res = fetch().await; // this doesn't work since the closure isn't async
        assert_eq!(2, res);
    })
    .await;
}

This is so I don't have to worry about forgetting to call the cleanup function. It's a pretty common pattern overall so I'd like to make it work.

I've been banging my head on this for a few hours, however I see I'm stuck so I'll have to improve my Rust knowledge before finding the answer on my own.

I gathered I'm mainly interested in this feature, which would simplify things greatly: Tracking issue for `#![feature(async_closure)]` (RFC 2394) · Issue #62290 · rust-lang/rust · GitHub

However since it's not ready, I'd appreciate if somebody could give me a little nudge and point me in the right direction.

Thanks :slight_smile:

An async function is really just a regular function that returns something that implements std::future::Future, so first you'll want to make x an async function:

async fn within_transaction<F>(x: fn(u8) -> F)
where
    F: Future<Output = ()>,
{
    let conn_id = init_stuff().await;
    x(conn_id).await;
    cleanup().await;
}

As for the closure, we can make the closure return a future using an async block, essentially making it an async function:

    within_transaction(|conn_id| async move {
        println!("--> conn_id={}", conn_id);
        assert_eq!(1, conn_id);

        let res = fetch().await;
        assert_eq!(2, res);
    })
    .await;

We need the move in addition to async to move conn_id into the async block, without it we get an error similar to what you might get if you forget a move before a closure that needs it:

error[E0373]: async block may outlive the current function, but it borrows `conn_id`, which is owned by the current function
  --> src/lib.rs:30:40
   |
30 |       within_transaction(|conn_id| async {
   |  ________________________________________^
31 | |         println!("--> conn_id={}", conn_id);
   | |                                    ------- `conn_id` is borrowed here
32 | |         assert_eq!(1, conn_id);
33 | |
34 | |         let res = fetch().await;
35 | |         assert_eq!(2, res);
36 | |     })
   | |_____^ may outlive borrowed value `conn_id`
   |
note: async block is returned here
  --> src/lib.rs:30:34
   |
30 |       within_transaction(|conn_id| async {
   |  __________________________________^
31 | |         println!("--> conn_id={}", conn_id);
32 | |         assert_eq!(1, conn_id);
33 | |
34 | |         let res = fetch().await;
35 | |         assert_eq!(2, res);
36 | |     })
   | |_____^
help: to force the async block to take ownership of `conn_id` (and any other referenced variables), use the `move` keyword
   |
30 |     within_transaction(|conn_id| async move {
   |                                        ++++

For more information about this error, try `rustc --explain E0373`.
1 Like

Thanks a lot @Heliozoa!

I got into a dead end trying to write something close to this at some point:

async fn within_transaction(x: fn(u8) -> std::future::Future<Output = ()>)
{
    let conn_id = init_stuff().await;
    x(conn_id).await;
    cleanup().await;
}

So I see the function needs to be generic then. I don't quite understand why yet, I'll have to read more about it.

Thanks again!

This is because you're trying to express that x has to be "something" that is a closure (or function) that takes a u8 and returns a Future with no value. Future is a trait, not a "materialized" type (a struct or enum), so the compiler needs to be told that you want something that returns Future, but that is determined by whoever calls within_transaction.

I'm assuming you ended up with code that looks like this: Rust Playground

Thanks for the feedback @ekuber. I'll use the Rust playground next time, looks very useful.

So if I understand you correctly, I can take this mental shortcut:

If passed parameter is a trait
    => then use the generics construct

Is that correct?

I wanted to rollback database state when integration testing. I got this:

I love it ^^

1 Like

That's a reasonable way to think of it, yes.

Good to know, thanks @ekuber :slight_smile: