asynchronoushaskellrustfuturerust-tokio

Rust async 'with_resource' (bracket) pattern


The problem

I'm looking for something similar to Haskell's bracket, that is:

But I encountered certain problems when trying to make this implementation asynchronous.

An attempt

Here's the furthest I've gotten:

use std::pin::Pin;
use std::boxed::Box;
use std::io;
use tokio::runtime::Runtime;

struct MyStruct;

struct MyResource {
    data: String,
}

impl MyResource {
    pub async fn async_cleanup(&self) -> Result<(), io::Error> {
        Ok(())
    }
}

impl MyStruct {
    pub async fn acquire_resource(&self) -> Result<MyResource, io::Error> {
        Ok(MyResource {
            data: "Hello, world!".to_string(),
        })
    }

    /*
    pub async fn with_resource<'a, A, F, Fut>(&self, action: F) -> Result<A, io::Error>
    where
        F: FnOnce(&'a mut MyResource) -> Fut,
        Fut: std::future::Future<Output = Result<A, io::Error>> + 'a,
    {
        let mut resource = self.acquire_resource().await?;
        let rv = action(&mut resource).await;
        resource.async_cleanup().await?;
        rv
    }
    */

    pub async fn with_resource_boxed<A, F>(&self, action: F) -> Result<A, io::Error>
    where
        F: for<'a> FnOnce(&'a mut MyResource) -> Pin<Box<dyn std::future::Future<Output = io::Result<A>> + 'a>>,
    {
        let mut resource = self.acquire_resource().await?;
        let rv = action(&mut resource).await;
        resource.async_cleanup().await?;
        rv
    }
}

fn main() {
    let my_struct = MyStruct {};

    async fn some_action(resource: &mut MyResource) -> Result<String, io::Error> {
        Ok(format!("message: {}", resource.data))
    }

    let future = my_struct.with_resource_boxed(|x| {
        Box::pin(async {
            some_action(x).await
        })
    });

    let rt = Runtime::new().unwrap();
    let result = rt.block_on(future);

    println!("Result: {:?}", result);
}

The compilation errors

Here are the 2 compilation errors when you uncomment with_resource:

error[E0597]: `resource` does not live long enough
  --> src/main.rs:31:25
   |
25 |     pub async fn with_resource<'a, A, F, Fut>(&'a self, action: F) -> Result<A, io::Error>
   |                                -- lifetime `'a` defined here
...
30 |         let mut resource = self.acquire_resource().await?;
   |             ------------ binding `resource` declared here
31 |         let rv = action(&mut resource).await;
   |                  -------^^^^^^^^^^^^^-
   |                  |      |
   |                  |      borrowed value does not live long enough
   |                  argument requires that `resource` is borrowed for `'a`
...
34 |     }
   |     - `resource` dropped here while still borrowed
error[E0502]: cannot borrow `resource` as immutable because it is also borrowed as mutable
  --> src/main.rs:32:9
   |
25 |     pub async fn with_resource<'a, A, F, Fut>(&'a self, action: F) -> Result<A, io::Error>
   |                                -- lifetime `'a` defined here
...
31 |         let rv = action(&mut resource).await;
   |                  ---------------------
   |                  |      |
   |                  |      mutable borrow occurs here
   |                  argument requires that `resource` is borrowed for `'a`
32 |         resource.async_cleanup().await?;
   |         ^^^^^^^^ immutable borrow occurs here

Questions

  1. Now, I don't understand why the compiler can't see 'a as the same 'a between the two where clauses? While it works when it's a single clause, with boxing.

  2. When it's not a higher order function, but I copy-paste the code from inside with_resource to another function, and call a non-lambda action, it works fine without boxing, so it should be possible somehow?

  3. How to solve this? Is it impossible without boxing?

  4. Could unsafe help?

  5. Would you recommend an alternative pattern for this? My clean-up code is async. I'd rather not depend on users remembering to call it.


Solution

  • Welcome to the bear-trap-laiden swampland that is async Rust. You have run into what is essentially the exact same issue as this guy here. The trick is to use a trait combined with a blanket impl to express the lifetime bounds that would be otherwise unexpressible. I won't pretend I came up with this idea, but here's it adapted to your use case:

    The trait:

    trait ActionFn<'a, O> {
        type Output: Future<Output = O>;
        fn call(self, res: &'a mut MyResource) -> Self::Output;
    }
    impl<'a, O, F, Fut> ActionFn<'a, O> for F
    where
        F: FnOnce(&'a mut MyResource) -> Fut,
        Fut: Future<Output = O>,
    {
        type Output = Fut;
        fn call(self, res: &'a mut MyResource) -> Self::Output {
            self(res)
        }
    }
    

    The method:

    pub async fn with_resource<A, F>(&self, action: F) -> Result<A, io::Error>
    where
        for<'a> F: ActionFn<'a, Result<A, io::Error>>,
    {
        let mut resource = self.acquire_resource().await?;
        let rv = action.call(&mut resource).await;
        resource.async_cleanup().await?;
        rv
    }
    

    Notice how ActionFn<'a, O>::Output and ActionFn<'a, O>::call(&self, &'a mut MyResource) are now bound by the same 'a.


    Note that at the call site, it is currently only possible to pass in an async function, but not a closure. I do recall having a similar problem while working with Actix, so it's definitely a broader issue with Rust itself.

    My best guess is that this is a lifetime variance bug and should be valid, but there could be some capturing funniness that I overlooked so don't quote me on this. Regardless, you may be interested in adding it to an entire shopping list of known async lifetime issues.