R Rust by Evidence PDF

Chapter 13 — Lifetimes Are API Relationships

Goal: Read lifetime parameters as relationships among references, not as instructions to keep values alive.

Here is the classic function whose intended relationship is missing:

fn longer(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}

Prediction

What does the compiler need?

A. A heap allocation for the result.
B. A lifetime connecting the returned reference to the inputs.
C. Both inputs changed to String.
D. A 'static annotation.

RevealB

Representative evidence:

error[E0106]: missing lifetime specifier
  --> ... -> &str
  = help: this function's return type contains a borrowed value,
    but the signature does not say whether it is borrowed from `a` or `b`

The function body clearly chooses one input, but callers are checked from the signature. The smallest fix states that both accepted input borrows and the output share a lifetime parameter:

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

This does not extend either string's life. At a call site, 'a becomes a lifetime no longer than both input borrows. Therefore the returned reference may be used only within their overlap.

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

fn main() {
    let outer = String::from("outside");
    let result;
    {
        let inner = String::from("in");
        result = longer(&outer, &inner);
        println!("{result}"); // valid inside the overlap
    }
    // println!("{result}"); // rejected: it might refer to `inner`
}

Working model: a lifetime annotation draws a line between borrowed inputs and outputs.

Precise model: a reference type includes a region during which it is valid. A generic lifetime parameter constrains those regions. fn longer<'a>(...) -> &'a str promises the output is valid for the chosen 'a; because either input may be returned, each input must satisfy that promise. Lifetimes describe provenance and valid use. They do not alter runtime behavior and usually generate no runtime data.

Lifetime elision is why most functions need no written annotations. In fn first(s: &str) -> &str, the single input reference lifetime is assigned to the output. In methods, an output reference is usually tied to &self. Elision is a fixed signature shorthand, not inference from arbitrary function bodies.

Struct lifetimes express the same relationship:

struct Heading<'a> {
    text: &'a str,
}

impl<'a> Heading<'a> {
    fn text(&self) -> &str {
        self.text
    }
}

Heading<'a> cannot outlive the text it borrows. It does not own that text. Use this when borrowing avoids copying and the owner naturally outlives the view.

A tradeoff alternative is to return ownership:

fn longer_owned(a: &str, b: &str) -> String {
    if a.len() >= b.len() { a.to_owned() } else { b.to_owned() }
}

This is simpler for callers to store because the result is independent, but it allocates and copies. Do not add lifetimes merely to avoid every clone; choose borrowing when the API's natural meaning is a view into caller-owned data.

Avoid reaching for 'static as a repair. &'static str means the referenced data is valid for the entire program, as string literals are. It does not mean “keep this local reference as long as needed.” Requiring 'static would reject ordinary borrowed String data rather than explain its relationship.

Mini challenge

Which signature can return the first argument without unnecessarily tying it to the second?

A. fn first<'a>(a: &'a str, b: &'a str) -> &'a str
B. fn first<'a, 'b>(a: &'a str, b: &'b str) -> &'a str
C. fn first(a: &str, b: &str) -> &'static str
D. No borrowed signature can do this.

Answer: B. The output comes only from a, so its lifetime should relate only to a. In real code write fn first<'a>(a: &'a str, _b: &str) -> &'a str; the second lifetime can be elided because it is unrelated. Option A compiles, but can shorten the usable output to the overlap with b.