I'm looking for something similar to Haskell's bracket
, that is:
with_resource(action: Resource -> A) -> A
(pseudo-code),action
from Resource
to a future of A
, and returns a future of A
,Resource
.But I encountered certain problems when trying to make this implementation asynchronous.
Here's the furthest I've gotten:
with_resource_boxed
works fine, but is a little awkward to use, and you have to pay the cost of boxing,with resource
doesn’t work, failing with a lifetime error (below).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);
}
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
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.
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?
How to solve this? Is it impossible without boxing?
Could unsafe
help?
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.
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.