Coding by Hand
Rust home

Lifetimes

Every reference in Rust is a loan slip. The slip has a name written on the front, a book it points to, and a due-date stamped in the corner. The book itself sits on the shelf and owns the words on its pages. Your slip lets you read those words for a while. When the book gets reshelved, every slip pointing at it has to be torn up first. The little tick marks like 'a and 'b you keep seeing in function signatures are the due-date stamps. They look strange because they are doing something no other mainstream language asks the programmer to write down — proving to the compiler that no slip ever outlives the book it points to.

A library loan slip with a due-date stamp standing in for a Rust reference's lifetime.
A library loan slip with a due-date stamp standing in for a Rust reference's lifetime.

The whole idea is borrowed from a thread of research called region-based memory management. In 1994 two compiler people, Mads Tofte and Jean-Pierre Talpin, were sitting at INRIA in France trying to free memory in a functional language without a garbage collector. They invented "regions" — chunks of memory that all expired at the same moment. Every allocation got a region tag, and the compiler proved no pointer escaped its region. A few years later a Cornell team built a language called Cyclone that grafted regions onto C, so you got pointers without the segfaults. Graydon Hoare started Rust at Mozilla in 2010 and kept the region idea but flipped the labor — instead of having the compiler infer every region, he made the programmer write the tag in the signature. That tag is 'a. The single quote is just how Rust marks "this is a lifetime, not a type."

Here is the function the whole lesson hangs on. It takes 2 string slices, hands back the longer one. The slip you get back has to come from one of the 2 slips you handed in.

fn longer<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() {
        left
    } else {
        right
    }
}

The <'a> after the function name introduces a lifetime parameter, the same way <T> introduces a type parameter. The first &'a str says "the first slip is good for some region we will call 'a." The second &'a str reuses the same name — so both inputs are good for the same region. The return type &'a str says the slip coming out is also good for that same region. That is the whole proof. The compiler reads it as: whoever calls longer must have a single chunk of program time during which both slips are still valid, and the slip I hand back will not outlive that chunk. The compiler does not pick 'a. The caller does, every time they call.

Watch the call site. Both books are owned by String values that live to the bottom of main. So the chunk 'a becomes the body of main from the moment both are created to the moment they fall off the end. The slip Rust hands back, winner, is valid for that whole region — which is exactly when the print statements run.

fn main() {
    let title_a = String::from("Dune");
    let title_b = String::from("The Selfish Gene");

    let winner = longer(&title_a, &title_b);

    println!("loan slip 'a: both books on loan");
    println!("  card -> {title_a}");
    println!("  card -> {title_b}");
    println!("librarian returns: {winner}");
    println!();

    diagram();
}

Run it, and the librarian hands back the longer book, then prints the loan slip diagram.

loan slip 'a: both books on loan
  card -> Dune
  card -> The Selfish Gene
librarian returns: The Selfish Gene

region 'a (start to end of main)
|
|  +-- title_a owns "Dune"
|  +-- title_b owns "The Selfish Gene"
|
|  &title_a  ----+
|                 |  longer<'a>(&'a, &'a) -> &'a
|  &title_b  ----+--> winner
|
|  winner is good for all of 'a, so this print is safe.
v
end of 'a: title_a and title_b drop together

The bottom half of the output is the part you want to stare at. The vertical bar is the region 'a. The two books are born at the top of the bar and dropped at the bottom of the bar. The two slips into longer are pinned to the same bar, and the slip out (winner) is pinned to the same bar too. The whole rectangle has to fit together with no slip dangling below its book. The compiler checks this shape at every call site before it lets the program build.

fn diagram() {
    println!("region 'a (start to end of main)");
    println!("|");
    println!("|  +-- title_a owns \"Dune\"");
    println!("|  +-- title_b owns \"The Selfish Gene\"");
    println!("|");
    println!("|  &title_a  ----+");
    println!("|                 |  longer<'a>(&'a, &'a) -> &'a");
    println!("|  &title_b  ----+--> winner");
    println!("|");
    println!("|  winner is good for all of 'a, so this print is safe.");
    println!("v");
    println!("end of 'a: title_a and title_b drop together");
}

Try this thought experiment — what happens if you write the function without the 'a tags? The compiler refuses, but its complaint is more interesting than just "missing annotation." It cannot guess which slip you mean to hand back. If the first input lives until Tuesday and the second lives until Friday, the slip Rust returns has to be valid for whichever shorter one the caller cares about. With no tag, there is no way to write "the answer lives as long as the shorter of the inputs." With the tag, both inputs share one region and the answer rides along. The single tag is doing real work — it ties the 3 slips together so the compiler can prove a slip never outlives its book.

The region 'a as a tall rectangle that must contain every reference tied to it.
The region 'a as a tall rectangle that must contain every reference tied to it.

Most of the time you never write 'a at all. Niko Matsakis, who ran the Rust borrow checker for years, kept watching new programmers get tripped up by the syntax and added a set of rules called lifetime elision. The rules cover the common cases — one input reference, one output, both the same lifetime — so the compiler fills in the tags for you and you write fn first(line: &str) -> &str. The tags are still there, just invisible. The moment a function takes 2 references and returns 1 of them, elision gives up and asks you to write the 'a yourself, because it does not know which input you mean.

Next lesson — what to do when a slip really does need to outlive every owner in sight, which is where Rust hands you a smart pointer instead of a reference.