Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does capturing an Arc by move make my closure FnOnce not Fn

Tags:

closures

rust

In the below example, I'm using an Arc to reference server state from a request handler, but the compiler make the closure an FnOnce. It feels like I'm doing the right thing in that each closure owns a strong reference to the state. Why doesn't this work? What options are there to make it work? Other questions like Share Arc between closures indicate something like this working, but I'm making the per-closure clones as shown and still getting an error.

#![feature(async_closure)]

#[derive(Default, Debug)]
struct State {}

impl State {
    pub async fn exists(&self, key: &str) -> bool {
        true
    }

    pub async fn update(&self, key: &str) {}
}

#[tokio::main]
async fn main() {
    use warp::Filter;
    use std::sync::Arc;

    let state: Arc<State> = Arc::default();

    let api = warp::post()
        .and(warp::path("/api"))
        .and(warp::path::param::<String>().and_then({
            let state = Arc::clone(&state);
            async move |p: String| {
                let x = state.exists(&p);
                if x.await {
                    Ok(p)
                } else {
                    Err(warp::reject::not_found())
                }
            }
        }))
        .and_then({
            let state = Arc::clone(&state);
            async move |id: String| {
                state.update(&id).await;
                Result::<String, warp::Rejection>::Ok("".to_owned())
            }
        });

    warp::serve(api).run(([127, 0, 0, 1], 0)).await;
}
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
  --> src/main.rs:25:13
   |
23 |           .and(warp::path::param::<String>().and_then({
   |                                              -------- the requirement to implement `Fn` derives from here
24 |               let state = Arc::clone(&state);
25 |               async move |p: String| {
   |  _____________^^^^^^^^^^^^^^^^^^^^^^_-
   | |             |
   | |             this closure implements `FnOnce`, not `Fn`
26 | |                 let x = state.exists(&p);
27 | |                 if x.await {
28 | |                     Ok(p)
...  |
31 | |                 }
32 | |             }
   | |_____________- closure is `FnOnce` because it moves the variable `state` out of its environment
like image 422
devnev Avatar asked Jun 15 '20 07:06

devnev


1 Answers

Well, async closures are unstable, so that may be a bug. I think that when the async block captures the Arc it consumes it, so the closure can actually only be called once. I mean, an async closure is some kind of generator: each time you call it it constructs a future, and it is the future which keeps the captured values.

As a workaround you may write something like this:

let state = Arc::clone(&state);
move |p: String| {
    let state = Arc::clone(&state);
    async move {
        let x = state.exists(&p);
        //...
    }
}

Doing another clone inside the closure but before the async block ensures that you can call the closure as many times as you need.

I would argue that actually Warp::Filter should accept a FnOnce to begin with, but I don't know warp enough to be sure.

like image 69
rodrigo Avatar answered Oct 19 '22 02:10

rodrigo