Search code examples
rustclosuresactix-web

Returning closure from function inconsitent with defining in place


While trying to implement a simple web server application using actix-web, I encountered apparently inconsistent behavior of Rust closures that do not know how to explain.

I had the following code:

use actix_web::{web, App, HttpServer};

#[derive(Clone)]
struct Config {
    val1: String,
    val2: String,
    val3: String,
}

fn main() {
    let conf = Config {
        val1: "just".to_string(),
        val2: "some".to_string(),
        val3: "data".to_string(),
    };

    HttpServer::new(move ||
        App::new().configure(create_config(&conf))
    )
        .bind("127.0.0.1:8088")
        .unwrap()
        .run()
        .unwrap();
}

fn create_config<'a>(conf: &'a Config) -> impl FnOnce(&mut web::ServiceConfig) + 'a {
    move |app: &mut web::ServiceConfig| {
        // Have to clone config because web::get().to by definition requires
        // its argument to have static lifetime, which is longer than 'a
        let my_own_conf_clone = conf.clone();
        app.service(
            web::scope("/user")
                .route("", web::get().to(move || get_user(&my_own_conf_clone)))
        );
    }
}

fn get_user(conf: &Config) -> String {
    println!("Config {} is {} here!", conf.val3, conf.val1);
    "User McUser".to_string()
}

This code works. Notice the closure I pass to web::get().to. I used it to pass the Config object down to get_user and still present web::get().to with a function that has no arguments, as it requires. At this point I decided to move the closure generation to a separate function:

fn create_config<'a>(conf: &'a Config) -> impl FnOnce(&mut web::ServiceConfig) + 'a {
    move |app: &mut web::ServiceConfig| {
        app.service(
            web::scope("/user")
                .route("", web::get().to(gen_get_user(conf)))
        );
    }
}

fn gen_get_user(conf: &Config) -> impl Fn() -> String {
    let my_own_conf_clone = conf.clone();
    move || get_user(&my_own_conf_clone)
}

fn get_user(conf: &Config) -> String {
    println!("Config {} is {} here!", conf.val3, conf.val1);
    "User McUser".to_string()
}

This code fails to compile with the following error:

error[E0277]: the trait bound `impl std::ops::Fn<()>: actix_web::handler::Factory<_, _>` is not satisfied
  --> src/main.rs:30:39
   |
30 |       .route("", web::get().to(gen_get_user(conf)))
   |                             ^^ the trait `actix_web::handler::Factory<_, _>` is not implemented for `impl std::ops::Fn<()>`

Why does it fail in the second case but not in first? Why was the trait Factory satisfied in the first case but isn't in the second? May be it's the factory's (its sources are here) fault? Is there a different way to return a closure, that would work in this situation? Any other approaches you could suggest? (Notice that Factory is not public so I cannot implement it directly myself)

If you wish to fool around with the code, I have it here: https://github.com/yanivmo/rust-closure-experiments Notice that you can move between the commits to see the code in its working or failing states.


Solution

  • Then using impl Trait as return type all other type information other then that value implements Trait get erased.

    In this particular case closure move || get_user(&my_own_conf_clone) implements Fn() -> String and Clone, but after returning Clone get erased.

    But since Factory implemented for Fn() -> String + Clone, and not for Fn() -> String return value no longer implement factory.

    This can be fixed by changing gen_get_user to

    fn gen_get_user(conf: &Config) -> impl Fn() -> String + Clone{
        let my_own_conf_clone = conf.clone();
        move || get_user(&my_own_conf_clone)
    }