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 
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 