Asynchronous wrapper function taking a reference

I'm trying to get the following code to compile, but rustc says that in transact function variable tr does not live long enough, and is dropped while being borrowed.

While I made this code to compile by either using macros or Arc's or moving the tr into the callback and having the callback return it back, the code below seems to me to be the simplest and most elegant solution (except that it does not compile, lol).

What puzzles me the most is why rustc insists on the lifetime 'tr to live outside of my function?(otherwise it wouldn't complain that tr is dropped while being borrowed, right?)

I'm also not sure which way it works: the lifetime of variable tr in the body of transact defines the generic lifetime 'tr or the other way round?

use std::future::Future;

use anyhow::Result;
use async_trait::async_trait;

#[tokio::main]
async fn main() {
    let db = TestDatabase;
    transact(&db, |tx| tx.calculation()).await.unwrap();
}

pub async fn transact<'db, 'tr, Db, Fun, Fut, Res>(db: &'db Db, callback: Fun) -> Result<Res>
where
    Db: Database,
    Db::Transaction: 'tr,
    Fun: FnOnce(&'tr mut Db::Transaction) -> Fut,
    Fut: Future<Output = Result<Res>>,
    Res: 'static, // marked this as static because I thought that there can be some relation between 'tr and result
{
    let mut tr = db.begin().await?;

    match callback(&mut tr).await {
        Ok(res) => {
            tr.commit().await?;
            Ok(res)
        }
        Err(e) => {
            tr.rollback().await?;
            Err(e)
        }
    }
}

#[async_trait]
pub trait Database {
    type Transaction: Transaction;
    async fn begin(&self) -> Result<Self::Transaction>;
}

#[async_trait]
pub trait Transaction {
    async fn commit(self) -> Result<()>;
    async fn rollback(self) -> Result<()>;
}

struct TestDatabase;

#[async_trait]
impl Database for TestDatabase {
    type Transaction = TestTransaction;

    async fn begin(&self) -> Result<Self::Transaction> {
        Ok(TestTransaction)
    }
}

struct TestTransaction;

impl TestTransaction {
    async fn calculation(&mut self) -> Result<i32> {
        Ok(42)
    }
}

#[async_trait]
impl Transaction for TestTransaction {
    async fn commit(self) -> Result<()> {
        Ok(())
    }

    async fn rollback(self) -> Result<()> {
        Ok(())
    }
}

Because you annotate 'tr on the function, which means the variable tr in the body needs to live for that long. So a HRTB, i.e. Fun: for<'tr> FnOnce(&'tr mut Db::Transaction) -> Fut, is required to express we want a tr with any lifetime in the body instead of one with a specified lifetime outside the function. Rust Playground

Then you'll run into another problem

error: lifetime may not live long enough
 --> src/main.rs:9:24
  |
9 |     transact(&db, |tx| tx.calculation()).await.unwrap();
  |                    --- ^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
  |                    | |
  |                    | return type of closure `impl Future<Output = Result<i32, anyhow::Error>>` contains a lifetime `'2`
  |                    has type `&'1 mut TestTransaction`

It's derived from the lifetime in async fn. Like

    async fn calculation(&mut self) -> Result<i32> {
        Ok(42)
    }

// desugar to
    fn calculation<'1>(&'1 mut self) -> impl '1 + Future<Output = Result<i32>> {
        async move { Ok(42) }
    }

So to change the default behaviour, you can do this Rust Playground

    fn calculation(&mut self) -> impl /* 'static + */ Future<Output = Result<i32>> { // lifetime elision :)
        async move { Ok(42) }
    }

But most of the time, you indeed need the default behaviour, since you use self there Rust Playground

    fn calculation(&mut self) -> impl /* 'static + */ Future<Output = Result<i32>> {
        async move { self; Ok(42) }
    }

// error :(
error[E0700]: hidden type for `impl Future<Output = Result<i32, anyhow::Error>>` captures lifetime that does not appear in bounds
  --> src/main.rs:60:9
   |
59 |     fn calculation(&mut self) -> impl /* 'static + */ Future<Output = Result<i32>> {
   |                    --------- hidden type `[async block@src/main.rs:60:9: 60:36]` captures the anonymous lifetime defined here
60 |         async move { self; Ok(42) }
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
help: to declare that `impl Future<Output = Result<i32, anyhow::Error>>` captures `'_`, you can add an explicit `'_` lifetime bound
   |
59 |     fn calculation(&mut self) -> impl /* 'static + */ Future<Output = Result<i32>> + '_ {
   |                                                                                    ++++

Then this is a common issue with async callbacks. Search in this forum [1] and you'll find many similar solution. Rust Playground

I won't repeat explaning here and there. If one is really curious about things, there are enough excellent explanations given by other masters.


  1. e.g. this recent one ↩ī¸Ž

3 Likes

Great and thorough answer. Thank you, Master!