I'm going to demonstrate the situation where I most often have friction with The Borrow Checker and provide a few patterns I use to make my code compile.

Let's pretend we're trying to connect to an imaginary Postgres database server using a connection pool. The API for this imaginary library requires us to first initialize a connection Pool with the connection string. Once the pool is initialized, we call connect() on it to get access to a usable, owned Connection for queries.

Why Returning References Can Be Difficult

Let's start with a naive approach that won't even compile:

// This won't compile!
fn connect(url: &str) -> &Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    &connection
}

The compiler will complain about the lifetime of &Connection not lasting long enough. Why? The compiler wants to deallocate the Connection object at the end of the function, but it knows that the &Connection reference we're trying to return points to that struct.

The Book does a great job of explaining owned values, borrowing, and lifetimes; but I'll provide my own version here. I'm doing a lot of generalizing and glossing over complexities in the next few paragraphs. If you understand why my sample code won't compile, feel free to skip ahead.

A Digression into Memory Allocation

A computer has a fixed amount of physical RAM which we typically refer to as "memory." As we write programs, all the data we process is represented in memory at some point during the execution of the program. We call the process of assigning memory to a specific running process "allocating" memory, and releasing ownership of memory "deallocating." In the simplest case, the newly allocated memory stores the data we assign to the variable. For other kinds of data, we store the memory address of another variable. We call this second kind of variable a "pointer" because it "points" to other memory. (I'll also use the term "reference" interchangeably with "pointer.") Pointers exist mostly to (1) avoid duplicating data in memory and (2) provide a predictably sized variable which represents unpredictably sized data (such as user input or a data from a network call).

A Brief Digression on Garbage Collectors

Many languages including Java and Python use a "garbage collector" to decide when a process should free memory. Every so often, the garbage collector will (1) interrupt whatever the programmer's code is doing, (2) hunt for memory that doesn't have pointers to it, (3) and free that memory. Garbage collectors operate at "runtime," while the program is running. This means that programmers can let the garbage collector figure out when to free memory automatically, which makes garbage collected languages easier and safer to write, but sometimes slower and with unpredictable memory usage.

Some languages don't use a garbage collector. In C, the programmer must decide when to free up memory manually, which is difficult to do correctly. If the programmer tries to use a pointer after the memory has been freed, Bad Things can happen -- things like crashing the program or allowing a hacker to gain root access. What makes Rust unique is that although it doesn't use a garbage collector, it still guarrantees that pointers will be safe to use.

How Rust Achieves Memory Safety without a Garbage Collector

The Rust compiler uses the metaphor of home "ownership." If memory is a house, then exactly one variable is the owner of that house and the death of that owner triggers an estate sale (aka deallocation). A variable owns memory when the programmer assigns data to it. When the owner goes out of scope and dies, that memory is deallocated. Because the compiler always knows when a variable goes out of scope, it always knows when to deallocate memory at compile time so Rust programs don't need to pause and garbage collect while running.

As in real estate, ownership can be transferred. Like selling a house, assigning one variable to another variable transers ownership. We say that the value has been "moved." Similar to how selling my home means moving my stuff, moves in Rust copy data to a new place in memory. This is called "move semantics."

You might think that if a large amount of data is moved then we're probably wasting RAM since the data must exist in at least two locations in memory. However, the Rust compiler heavily optimizes code when using the --release flag, so most of the time the compiler can see that we're about to waste memory and simply reuse the existing memory location instead of stupidly copying.

Another problem introduced by move semantics is that you often want many variables to be able to access the same data from different places in your program. Ownership would seem to make that impossible since only one variable can own a memory location at a time. However, Rust also allows "borrowing" of memory. The programmer borrows memory by creating a pointer to it. If memory is a house and one variable is the owner, pointers are the renters who temporarily use that memory. If the owner goes out of scope and dies, pointers can no longer legally use that memory. The Rust compiler acts like a property management agency by tracking all ownership lifetimes and borrowing to make sure that no memory is being squatted on illegally. This aspect of the Rust compiler is known as The Borrow Checker, and it's the thing which makes Rust uniquely Rust.

Back to the code sample

// This won't compile!
fn connect(url: &str) -> &Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    &connection
}

Going back to our example, there's only one scope involved: the entire body of fn connect() {..}. So, at the end of connect(), all variables declared inside the function will be deallocated. Invoking pool.connect() creates an unnamed Connection object in this scope, but because that object is created inside of the connect() scope, it also must be deallocated at the end of function. This means that after we return the pointer to the Connection, there won't be a Connection anymore! This is what the compiler is trying to tell us: that pointer will refer to freed memory after the end of the function.

Patterns for Returning References

Pattern 1: Return Owned Values

Give up on references, and just return a full copy of the value. Depending on the use case, this might be a good solution, and it's often the easiest way to appease The Borrow Checker.

fn connect(url: &str) -> Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    connection
}

There are two changes needed in this trivial case:

  • Remove the &.. in the return value and use .. instead
  • Remove the & in the function body

Some types, like str and Path are only intended to be used with references, and they have a sibling type which is intended to be used only as an owned value. For these types, if we just try removing the & from our code, we'll still get compile errors. Here's an example that won't compile:

// This won't compile!
fn returns_a_path() -> Path {
    let path: Path = somehow_makes_path();
    path
}

For these types which can only be used as references, look for an impl of the trait ToOwned. How it works is ToOwned consumes a shared reference and copies the value into a new owned reference. Here's an example of leveraging the ToOwned trait:

fn returns_a_pathbuf() -> PathBuf {
    let path: &Path = somehow_makes_path();
    let pathbuf: PathBuf = path.to_owned();
    pathbuf
}

Pros:

  • Low Effort - generally, converting a return value from a shared reference to an owned value is really easy. I can usually get something to compile quickly and refactor later using this technique.
  • Widely Applicable - almost anywhere we can return a reference, we could also return an owned copy of that value
  • Safe - a new copy of the value cannot corrupt memory elsewhere, even when moving the data between threads

Cons:

  • Synchronization - this value won't be changed if we change the original value.
  • Memory - we'll possibly be wasting memory by making identical copies of some piece of data (usually won't happen)

To me, using this technique often feels like a kludge because I've spoken to the Borrow Checker instead of expressing the core logic of my application. Think hard about how many places need to mutate the value in question or if they can all simply read the same value this way. If this is an unchanging value, returning an owned value can be a great solution.

Pattern 2: Return Boxed Values

Let's move the database connection off the function stack and into the heap. In our own types, we'll need to explicitly heap allocate data using the Box struct from the standard library. So, refactoring our above code to use the heap would look like this:

fn connect(url: &str) -> Box<Connection> {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    Box::new(connection)
}

There are two changes:

  • The function signature now has Box<..> as the return type instead of &..
  • The function body instantiates a new box wrapping the connection expression Box::new(..) instead of &..

The boxed value is an owned struct (not a reference) so it can be returned from the function without angering the borrow checker. When the box itself goes out of scope, the heap allocated memory will be freed.

Pros:

  • Memory - The only additional memory (beyond the boxed value) is the pointer to the heap, and pointers are only a few bytes
  • Applicability - Almost any code which uses std can leverage this technique

Cons:

  • Indirection - we'll need to write more code in our type annotations, and we may need to understand how to leverage the Deref trait to work with boxed values
  • Overhead - It's more complicated to allocate memory on the heap and this may incur a runtime penalty.

For types like usize, bool, f32, and other primitive types, it can be a code smell if I find myself boxing these values. Instead I normally return a copy of these types instead.

For dynamically growable types like Vec, String, and HashMap, these types already use the heap internally so you're not necessarily gaining much by boxing them. If performance or memory usage matter, profile your own code under realistic conditions to determine if boxing your return value improves or degrades performance relative to another pattern.

As with the previous pattern, you want to thing about mutability -- how many places am I reading this value? How many of those places am I going to be writing to this value as well? Boxing in conjunction with Atomic Reference Counting (Arc type) can make mutably sharing a value across threads possible if you need to. If you don't need that, boxing might just be slowing down your program or wasting memory.

Pattern 3: Move Owned Values to a Higher Scope

This technique involves reorganizing code to help us leverage references passed into functions. Here's an example:

fn setup_connection(connection: &Connection) -> &Connection {
   connection.set_database("startup.io");
   connection
}

fn main() {
    let pool = postgres::connection_pool::new(url);
    for _ in 0..10 {
      let connection = setup_connection(&pool.connect());
      // Do something with connection here
    }
}

We achieved this by:

  • moving both the Pool and Connection objects to a scope outside the function
  • changing the function signature to take in a reference to the connection we wanted to modify
  • returning a reference to the same memory as the memory borrowed in the argument

At the end of setup_connection(), no new structs were declared inside of it. This means nothing needs to be deallocated. Because connection was declared inside the for loop, it lives for one iteration of the loop. In the next iteration, a new connection is allocated. Basically, we can only get the lifetimes out of a function that we put into the function.

In this particular case, no matter how many times we call setup_connection(), only one Pool object will be allocated, compared to having many pools allocated in the other patterns.

Pros:

  • Memory - this pattern avoids heap allocating and writing boilerplate code, so it's the memory efficient and elegant
  • Aroma - this pattern is often the natural result of good code organization that reduces unnecessary work; its like a good code "aroma"

Cons:

  • Complexity - this pattern requires deep understanding of the application and data flows, and usually means rewriting several different areas of the code
  • Applicability - many times this pattern can't be used
  • Rigidness - using this pattern might make refactoring the code more difficult

This pattern is likely to work when we have an owned value which "emits" other borrowed objects. If we can move the owned struct to a higher scope, we can pass referencecs to helper functions. When I'm looking to refactor and/or reduce memory usage, I examine if this pattern might be an option.

Pattern 4: Use Callbacks not Return Values

This technique was suggested by mmstick on reddit. If you've ever written JavaScript with callbacks or used dependency injection in Java, you'll feel right at home here. The basic idea is to avoid fighting the Borrow Checker by passing a closure into the function. Example time:

fn connect_and_attempt<F>(pool: &Pool, action: F) -> Option<String>
    where F: Fn(Connection) -> Option<String>
{
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    action(connection)
}

fn main() {
    let pool: Pool = postgres::connection_pool::new(url);
    let result = connect(&pool, |connection| {
        // do something with connection and return an option
        Some(output)
    });
}

The key difference over pattern 3 is that we pass an anonymous function |connection| { .. } into connect_and_attempt and we never return the connection object at all. This means we didn't have to fight the Borrow Checker.

Pros:

  • Elegant - avoid fighting the borrow checker
  • Decoupled - helps isolate application logic from I/O or dependencies

Cons:

  • Inflexible - closures in Rust are more complicated than JavaScript and might require boxing in certain situations.
  • Complexity - requires a deeper knowledge of Rust and mastery of closures.

As noted by mmstick, this pattern can make unit testing really easy because we can decouple blocks of code and test their logic without setting up the full environment. We can pass dummy callbacks into the system under test.

Additional Resources

  • Reddit Comments -- I'm tremedously greateful to all of the many people who chimed in on Reddit about this article -- I learned so much!
  • Let's Clone a Cow - New Rustacean Podcast - Chris Krycho explains how to do some sophiscated things with memory management in Rust while satisfying the Borrow Checker

More patterns?

Please, if you have experience using more patterns than I've enumerated here, please share them! I'd love to incorporate your patterns into this article as a sort of reference of techniques for newbies on the Borrow Checker.