To Box or not to Box — My First Real Rust Refactor

Kevin Hoffman
5 min readJan 31, 2018

--

I’ve been attempting to improve my Rust skills with a side project. It’s one thing to attend conferences and read books and build hello world samples, but things don’t feel quite “right” unless I’m building something of actual value — a project that could be something that might make it into production someday.

You run into all kinds of problems and scenarios building a real project that you simply never encounter when building trivial examples. In my latest side project, I had a need to store and manage a list of what I call agents. These are worker processes (written in Rust) that are running and connecting to a central clearing house.

Inside this central clearing house, I need to keep track of the agents that are active and when they last contacted the server. Under the hood, I’m storing the agents list in the/agents/ directory key in an etcd cluster.

Seems pretty straightforward, so I start typing and creating abstractions. The first thing I come up with is an agent store. I want this to work against the live etcd cluster, but I also want it to work against an in-memory KV store so I can write unit tests, etc. I don’t know if this is idiomatic Rust, but this is how I would approach the problem in Go.

I start with something that looks like this:

pub struct AgentStore {
kvstore: KVStore
}

I decide that I want a trait called KVStore that represents the underlying things I can do with a key-value store like update and query. This way, I’ll have an EtcdStore and an InMemoryStore that both implement the KVStore trait and I can test the AgentStore in unit tests using the InMemoryStore as the supporting key-value storage.

I start getting compilation errors immediately. First I get errors because the gRPC generated code I’m using requires that any struct hosting a service be thread-safe. My service has an owned value of kvstore on it. The Rust compiler is going to go all the way down the chain and verify that everything is thread-safe. So if I have a thread-safe struct holding a thread-safe value which, after five nested values, contains some value that isn’t thread-safe, Rust won’t let me compile.

Between refactoring to make things thread-safe and fiddling with various ways to define my struct, I see errors about the use of KVStore as a type within the agent store struct. I switch that to a Box<KVStore> which does two things:

  • It moves the storage of the field to the heap
  • It allows more flexibility, allowing anything that implements the KVStore trait (e.g. trait object) to be boxed.

It looks like I’m on the right track and I start feeling super smart: I got the Rust compiler to reduce the number of errors! YAY! But my victory is short-lived. Now come the dreaded lifetime errors.

Rust can’t infer how long the AgentStore struct should live, so I now start dropping lifetime parameters on it:

pub struct AgentStore<'a> {
kvstore: Box<KVStore>
}

I don’t have the file history on me, so I’m only guessing at what I remember the syntax looked like at this point. Now I start positively littering my code with lifetime specifiers. Everything that uses the AgentStore needs a lifetime specifier like the AgentStore::new(kvstore) function. I smother the EtcdStore and InMemoryStore structs and implementations with 'a and finally after an hour or so of the torturous back and forth between myself and the compiler, I get it working.

The next morning I wake up and I think to myself: I’m just using the box as a hack to store a trait object. Do I really need a trait object? Can I use generics instead?

After a cup of coffee, I go back to the code and give it a fresh look. I change my AgentStore struct to look like this:

pub struct AgentStore<T> where T: KVStore {
kvstore: T
}
impl<T> AgentStore<T> where T: KVStore {
pub fn new(store:T) -> AgentStore<T> {
AgentStore{ kvstore: store }
}
}

This certainly looks cleaner than my previous code which was unreadable because of 'a being slathered over it like so much newbsauce. My implementations of the stores no longer have multiple lifetime parameters and they look pretty straightforward:

impl KVStore for EtcdStore {
fn set(&mut self, key: &str, value: &str) -> ....
}

In fact, all of my lifetime specifiers are gone. I get a bunch of errors about an unused or unnecessary lifetime everywhere in my code. So I gleefully remove all of them.

At the same time I’m doing this, I switch to the grpcio implementation of gRPC. This one requires that the service struct be Clone , so I am now confused about how to make all my stuff clone-able. Turns out it’s not that difficult, as I can use #[derive(Clone)] on my structs. To tell the generic code that everything from the service through my agent store and down is clone-able, I eventually make rustc happy by indicating that anything that is a KVStore is also Clone :

pub trait KVStore: Clone {
fn set(&mut self, key: &str, value: &str) -> Result<(), Box<Error>>;
fn get_from_dir(&self, key: &str) -> Result<HashMap<String,String>, Box<Error>>;
}

At this point I have no idea if what I’m doing is idiomatic Rust. What I do know is that I’ve made the code much easier to read and, in my opinion, this feels like a more appropriate use of generics. If I needed to store a vector of things that implement the KVStore trait, then I’d opt for a Vec<Box<KVStore>>. The treatment of the elements of the vector as a trait object is exactly the behavior I would need there.

In general I find refactoring to be a far more excruciating process in Rust than in other languages. It is also far more satisfying. If you can make your code look cleaner or run more efficiently and still manage to satisfy the brutal dictator that is the Rust compiler, then you legitimately feel some sense of accomplishment at the end of the day.

As I figure more of this stuff out I will post my experiences, but for now I am thoroughly enjoying the struggle to learn Rust while building something real.

--

--

Kevin Hoffman

In relentless pursuit of elegant simplicity. Tinkerer, writer of tech, fantasy, and sci-fi. Converting napkin drawings into code for @CapitalOne