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.
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)
}