As a learning exercise, I've been trying to write a certain style of "fluent" builder code in rust, where one creates an object with a reference to 'self' that later mutates the original object. I thought I'd be able to use lifetime subtyping to accomplish this, but I haven't had much luck.
I have two questions:
- Is there a way to get the borrow checker to respect what I'm trying to do; or, if there isn't, why?
- Regardless of (1), is there an obviously more idiomatic way to accomplish this?
Here is my code at the moment:
use std::collections::HashMap;
type Handler<'a> = Box<dyn Fn(i32) -> i32 + 'a>;
pub struct Router<'r> {
routes: HashMap<String, Handler<'r>>,
}
pub struct RouteAdder<'a, 'r: 'a> {
router: &'a mut Router<'r>,
route: String,
}
impl<'r> Router<'r> {
pub fn new() -> Self {
Router {
routes: HashMap::new(),
}
}
pub fn route<'a>(&'r mut self, route: String) -> RouteAdder<'a, 'r> {
RouteAdder::of(self, route)
}
pub fn send(&self, s: &str, x: i32) -> Option<i32> {
match self.routes.get(s) {
None => None,
Some(h) => Some(h(x)),
}
}
}
impl<'a, 'r> RouteAdder<'a, 'r> {
fn of(router: &'a mut Router<'r>, route: String) -> Self {
RouteAdder { router, route }
}
pub fn to(self, handler: impl Fn(i32) -> i32 + 'r) {
self.router.routes.insert(self.route, Box::new(handler));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route() {
let router = {
let mut router = Router::new();
router.route(String::from("hello")).to(|x| x + 1);
router
};
assert_eq!(router.send("hello", 3), Some(4));
}
}
The code here is inspired by the idea of creating http routers, but I'm mostly wondering if this sort of "pass reference back to self then mutate" code is possible. The easiest way to go about this would probably be to simply create a Route
type (which could probably be done fairly fluently) and pass that in to an add_route(&mut self, route: Route)
function to the Router
type.
Here is the output of cargo test
I get:
λ cargo test
Compiling rfluent v0.1.0 (<redacted>)
error[E0597]: `router` does not live long enough
--> src/lib.rs:51:13
|
51 | router.route(String::from("hello")).to(|x| x + 1);
| ^^^^^^ borrowed value does not live long enough
52 | router
53 | };
| -
| |
| `router` dropped here while still borrowed
| borrow might be used here, when `router` is dropped and runs the destructor for type `Router<'_>`
error[E0505]: cannot move out of `router` because it is borrowed
--> src/lib.rs:52:13
|
51 | router.route(String::from("hello")).to(|x| x + 1);
| ------ borrow of `router` occurs here
52 | router
| ^^^^^^
| |
| move out of `router` occurs here
| borrow later used here
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0505, E0597.
For more information about an error, try `rustc --explain E0505`.
error: Could not compile `rfluent`.
warning: build failed, waiting for other jobs to finish...
error: build failed
I feel like I have a surface-level understanding of what the problem is - that rust can't actually determine that the reference to router
"dies" when RouterAdder::to
is called - but my understanding doesn't go much deeper than that (and could just be wrong).