Writing "fluent" code, creating objects with reference to self and lifetime subtyping

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:

  1. Is there a way to get the borrow checker to respect what I'm trying to do; or, if there isn't, why?
  2. 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).

1 Like

Your lifetimes were mixed up. Lifetime names are variables too, give them good names when you have more than one going around.

use std::collections::HashMap;

type Handler<'a> = Box<dyn Fn(i32) -> i32 + 'a>;

pub struct Router<'handle> {
    routes: HashMap<String, Handler<'handle>>,
}

pub struct RouteAdder<'route, 'handle> {
    router: &'route mut Router<'handle>,
    route: String,
}

impl<'handle> Router<'handle> {
    pub fn new() -> Self {
        Router {
            routes: HashMap::new(),
        }
    }

    pub fn route<'route>(&'route mut self, route: String) -> RouteAdder<'route, 'handle> {
        RouteAdder::of(self, route)
    }

    pub fn send(&self, s: &str, x: i32) -> Option<i32> {
        self.routes.get(s).map(|h| h(x))
    }
}

impl<'route, 'handle> RouteAdder<'route, 'handle> {
    fn of(router: &'route mut Router<'handle>, route: String) -> Self {
        RouteAdder { router, route }
    }

    pub fn to(self, handler: impl Fn(i32) -> i32 + 'handle) {
        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 problematic function was

pub fn route<'a>(&'r mut self, route: String) -> RouteAdder<'a, 'r>

Because you are trying to tie the lifetime of the handler to self and 'a does nothing, but later you assumed that 'a was the lifetime of the route and 'r was the lifetime of the handler. This confusion is what caused the error.

Note: route could be simplified to

pub fn route(&mut self, route: String) -> RouteAdder<'_, 'handle>

Due to lifetime elision rules

3 Likes

More than just getting the lifetimes mixed up, I think there was an error in how I thought about their semantics. (If I had given my lifetimes proper names, perhaps that would have been more obvious!) I will have to redo some reading on the matter.

I was thinking something like this:

pub struct RouteAdder<'adder, 'router: 'adder> {
    router: &'adder mut Router<'router>,
    route: String,
}

My though process was "The RouteAdder, with its own lifetime and a router lifetime lasting at least as long, has router: a reference (lasting as long as the Adder) to a router (with its own lifetime).

The way you've written it is more like so: "The RouteAdder, given a router lifetime and a handler lifetime, has router: a reference to a router (lasting a given lifetime) with its own handler lifetime".

In the Router definition, I would normally have called 'handler 'router instead, to signify my intention that the handler box last only as long as the router itself.

Maybe these are both valid models? In that case, perhaps what I'm missing is the fact that the route function can take a reference to self with a lifetime that is more specific than it normally need be?

Either way, I appreciate the quick response! It will definitely point me the right way as I re-learn what I thought I knew.

The lifetime in Box<_> is the lifetime of the handler function, not of the router. If you want it to be the lifetime of the router, you'll be fighting the borrow checker every step of the way because you will be trying to create a self-referential type. This is extremely hard to do correctly.

They are semantically very different, so it depends on what you want. In this case I think my solution is a better fit.

Here's how I thought of it, starting from Handler

  • Handler has some lifetime to the captured environment, so let's call it's lifetime 'a
    • Handler isn't particularly complex, so 'a is a fine lifetime here
  • Router need a lifetime because Handler has a lifetime, let's call it's lifetime 'handler
    • 'env would be a better name
  • RouteAdder has a reference to a Router, so it need two lifetimes, one for the environment as specified by Handler, and the other for the router it self. The lifetime associated with the environment will be 'handler to stay consistent. The lifetime of the router will be 'router to remind us where we got it from, which is in turn what value's lifetime it specifies.
  • RouteAdder::of is a constructor, so it will just copy the lifetimes from type definition
  • RouteAdder::to takes a handler, so I need to mark the handler as having the lifetime of the environment (which was named 'handler)
  • Route::route needs to pass a reference to itself to RouteAdder, and so the lifetime of RouterAdder::route should be 'route to match up with &mut self. The environment lifetime should just be passed along.

Here is a way to think of lifetime so that you don't fight the borrow checker.

  • When you name lifetime, name it with the pointee in mind, not the pointer. This is because...
  • Generic lifetime parameters (like 'a or 'router) signify data that lives somewhere else, i.e. the pointee. So in &'a mut T, 'a signifies the lifetime of T, not &'a mut T. This in turn is how long &'a mut T is valid for, so it's lifetime cannot outlive 'a.
  • A lifetime parameter on a type definition is almost always specifies the lifetime of something that the type does not own. This can be some value (like router in RouteAdder or some allocation in fancier systems enabled by arenas)
1 Like

Ah. I was thinking that Router would be responsible for dropping the Box as it seems to own it, but I can imagine situations in which a closure could references to objects with lifetimes that outlive the router. I'll play with this idea more.

It is responsible for dropping the Box, but as you noted the Box may not own all of the data it references. This means that there is a lifetime associated with the Box.

simple example,

let mut counter = Vec::new();

let handler: Box<dyn FnMut(i32) -> i32> = Box::new(|x| {
    let rand: i32 = random();
    counter.push(rand);
    x + counter.iter().sum::<i32>()
});

In this case the handler doesn't own counter, but still references it, so it must not outlive counter otherwise it would contain a dangling reference.

1 Like

And the pieces begin to fall into place. Cheers, thanks again.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.