ExamplesBy LevelBy TopicLearning Paths
106 Intermediate

106-lifetime-elision — Lifetime Elision Rules

Functional Programming

Tutorial

The Problem

Lifetime annotations are often redundant — the compiler can infer them from context. Rust's three lifetime elision rules specify exactly when annotations can be omitted, making the common cases concise while requiring explicit annotations only for ambiguous cases.

Understanding elision rules helps you read Rust code that lacks annotations and know when you need to add them. It also explains why fn first_word(s: &str) -> &str compiles without any 'a.

🎯 Learning Outcomes

  • • Know the three elision rules by name and understand what each covers
  • • Recognize when a function's lifetimes are inferred vs must be explicit
  • • Understand rule 3 (self lifetime) for methods on structs
  • • Know when two input references require an explicit output lifetime
  • • Read and write lifetime annotations only when the rules do not apply
  • Code Example

    // Compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
    fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or(s)
    }
    
    struct TextBuffer { content: String }
    
    impl TextBuffer {
        // Compiler infers: fn get_content<'a>(&'a self) -> &'a str
        fn get_content(&self) -> &str { &self.content }
        fn get_length(&self) -> usize { self.content.len() }
    }

    Key Differences

  • Elision scope: Rust elision rules cover the most common patterns; explicit annotations are needed for any case beyond them. OCaml needs no annotations at all.
  • Ambiguity: Two input references in Rust create ambiguity that the compiler reports as an error if the output is a reference; OCaml has no such ambiguity.
  • Rule 3 value: The &self lifetime rule makes virtually all method return values annotation-free, which is why most Rust struct methods look clean.
  • Learning curve: Rust programmers need to internalize the three rules; OCaml programmers have no equivalent concept to learn.
  • OCaml Approach

    OCaml has no lifetime annotations and no elision rules — all references are managed by the GC with no lifetime tracking:

    let first_word s = String.split_on_char ' ' s |> List.hd
    
    (* Two inputs — OCaml makes no distinction *)
    let pick_first a _b = a  (* GC tracks both; no lifetime concern *)
    

    All OCaml functions return values that the GC will keep alive as long as needed. The programmer never needs to annotate how long a return value borrows from an argument.

    Full Source

    #![allow(clippy::all)]
    // Example 106: Lifetime Elision Rules
    //
    // Rust has three elision rules that let you skip lifetime annotations
    // in the most common cases:
    //
    //   Rule 1: Each input reference gets its own distinct lifetime.
    //   Rule 2: If there is exactly one input lifetime, every output
    //           reference gets that same lifetime.
    //   Rule 3: If one of the inputs is &self or &mut self, every output
    //           reference gets self's lifetime.
    //
    // When the rules produce an unambiguous answer you write nothing.
    // When they don't, the compiler asks you to be explicit.
    
    // ── Approach 1: single input reference (rule 2 applies) ──────────────
    // The compiler expands this to:
    //   fn first_word<'a>(s: &'a str) -> &'a str
    // You write nothing — the relationship is obvious.
    pub fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or(s)
    }
    
    // Two input references → rule 2 cannot apply (ambiguous source).
    // We must spell out which input the output borrows from.
    pub fn pick_first<'a>(a: &'a str, _b: &str) -> &'a str {
        a
    }
    
    // ── Approach 2: method with &self (rule 3 applies) ───────────────────
    // The compiler expands `fn get_content(&self) -> &str` to
    //   fn get_content<'a>(&'a self) -> &'a str
    pub struct TextBuffer {
        content: String,
    }
    
    impl TextBuffer {
        pub fn new(content: &str) -> Self {
            Self {
                content: content.to_owned(),
            }
        }
    
        // Rule 3: output borrows from self — no annotation needed.
        pub fn get_content(&self) -> &str {
            &self.content
        }
    
        pub fn get_length(&self) -> usize {
            self.content.len()
        }
    
        // Rule 3 still applies even when we also take another reference.
        // The returned slice is guaranteed to live as long as `self`.
        pub fn trim_to(&self, max: usize) -> &str {
            let end = max.min(self.content.len());
            &self.content[..end]
        }
    }
    
    // ── Approach 3: struct holding a reference (explicit lifetime required)
    // Elision rules don't cover struct fields — you must write 'a.
    pub struct Excerpt<'a> {
        pub text: &'a str,
    }
    
    impl<'a> Excerpt<'a> {
        pub fn new(text: &'a str) -> Self {
            Self { text }
        }
    
        // Rule 3: output borrows from self, so the return gets self's lifetime.
        pub fn content(&self) -> &str {
            self.text
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── first_word ────────────────────────────────────────────────────
    
        #[test]
        fn test_first_word_with_space() {
            assert_eq!(first_word("hello world"), "hello");
        }
    
        #[test]
        fn test_first_word_single_word() {
            assert_eq!(first_word("hello"), "hello");
        }
    
        #[test]
        fn test_first_word_empty() {
            assert_eq!(first_word(""), "");
        }
    
        #[test]
        fn test_first_word_multiple_words() {
            assert_eq!(first_word("one two three"), "one");
        }
    
        // ── pick_first (explicit lifetime) ───────────────────────────────
    
        #[test]
        fn test_pick_first_returns_a() {
            let a = String::from("abcde");
            let result;
            {
                let b = String::from("xy");
                // result borrows from `a` (lifetime 'a), so b can end here.
                result = pick_first(&a, &b);
            }
            assert_eq!(result, "abcde");
        }
    
        // ── TextBuffer ────────────────────────────────────────────────────
    
        #[test]
        fn test_text_buffer_get_content() {
            let buf = TextBuffer::new("Hello, World!");
            assert_eq!(buf.get_content(), "Hello, World!");
        }
    
        #[test]
        fn test_text_buffer_get_length() {
            let buf = TextBuffer::new("Rust");
            assert_eq!(buf.get_length(), 4);
        }
    
        #[test]
        fn test_text_buffer_trim_to() {
            let buf = TextBuffer::new("lifetime elision");
            assert_eq!(buf.trim_to(8), "lifetime");
        }
    
        #[test]
        fn test_text_buffer_trim_to_beyond_length() {
            let buf = TextBuffer::new("short");
            assert_eq!(buf.trim_to(100), "short");
        }
    
        // ── Excerpt ───────────────────────────────────────────────────────
    
        #[test]
        fn test_excerpt_content() {
            let text = String::from("We choose to go to the Moon.");
            let ex = Excerpt::new(&text);
            assert_eq!(ex.content(), "We choose to go to the Moon.");
        }
    
        #[test]
        fn test_excerpt_text_field() {
            let s = "four score and seven years";
            let ex = Excerpt::new(s);
            assert_eq!(ex.text, s);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── first_word ────────────────────────────────────────────────────
    
        #[test]
        fn test_first_word_with_space() {
            assert_eq!(first_word("hello world"), "hello");
        }
    
        #[test]
        fn test_first_word_single_word() {
            assert_eq!(first_word("hello"), "hello");
        }
    
        #[test]
        fn test_first_word_empty() {
            assert_eq!(first_word(""), "");
        }
    
        #[test]
        fn test_first_word_multiple_words() {
            assert_eq!(first_word("one two three"), "one");
        }
    
        // ── pick_first (explicit lifetime) ───────────────────────────────
    
        #[test]
        fn test_pick_first_returns_a() {
            let a = String::from("abcde");
            let result;
            {
                let b = String::from("xy");
                // result borrows from `a` (lifetime 'a), so b can end here.
                result = pick_first(&a, &b);
            }
            assert_eq!(result, "abcde");
        }
    
        // ── TextBuffer ────────────────────────────────────────────────────
    
        #[test]
        fn test_text_buffer_get_content() {
            let buf = TextBuffer::new("Hello, World!");
            assert_eq!(buf.get_content(), "Hello, World!");
        }
    
        #[test]
        fn test_text_buffer_get_length() {
            let buf = TextBuffer::new("Rust");
            assert_eq!(buf.get_length(), 4);
        }
    
        #[test]
        fn test_text_buffer_trim_to() {
            let buf = TextBuffer::new("lifetime elision");
            assert_eq!(buf.trim_to(8), "lifetime");
        }
    
        #[test]
        fn test_text_buffer_trim_to_beyond_length() {
            let buf = TextBuffer::new("short");
            assert_eq!(buf.trim_to(100), "short");
        }
    
        // ── Excerpt ───────────────────────────────────────────────────────
    
        #[test]
        fn test_excerpt_content() {
            let text = String::from("We choose to go to the Moon.");
            let ex = Excerpt::new(&text);
            assert_eq!(ex.content(), "We choose to go to the Moon.");
        }
    
        #[test]
        fn test_excerpt_text_field() {
            let s = "four score and seven years";
            let ex = Excerpt::new(s);
            assert_eq!(ex.text, s);
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetime Elision

    Side-by-Side Code

    OCaml

    (* OCaml has no lifetime annotations — the GC handles memory safety *)
    let first_word s =
      match String.index_opt s ' ' with
      | Some i -> String.sub s 0 i
      | None -> s
    
    type text_buffer = { content : string }
    let get_content buf = buf.content
    let get_length buf = String.length buf.content
    

    Rust (idiomatic — elision applies)

    // Compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
    fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or(s)
    }
    
    struct TextBuffer { content: String }
    
    impl TextBuffer {
        // Compiler infers: fn get_content<'a>(&'a self) -> &'a str
        fn get_content(&self) -> &str { &self.content }
        fn get_length(&self) -> usize { self.content.len() }
    }
    

    Rust (explicit — when elision cannot resolve ambiguity)

    // Two input references: compiler cannot know which to tie the output to
    fn pick_first<'a>(a: &'a str, _b: &str) -> &'a str { a }
    
    // Struct holding a reference always requires explicit annotation
    struct Excerpt<'a> { text: &'a str }
    

    Type Signatures

    ConceptOCamlRust (elided)Rust (explicit)
    String slicestring&str&'a str
    Function (1 ref in → ref out)string -> stringfn f(s: &str) -> &strfn f<'a>(s: &'a str) -> &'a str
    Method returning borrowed fieldt -> stringfn m(&self) -> &strfn m<'a>(&'a self) -> &'a str
    Struct with borrowed fieldimpossible (GC owns)(must annotate)struct S<'a> { f: &'a str }

    Key Insights

  • No annotations vs. hidden annotations: OCaml's GC removes the need for lifetime reasoning entirely. Rust's elision rules make the most common lifetime relationships implicit — the annotations exist, the compiler just fills them in.
  • Three deterministic rules: (1) each input ref gets its own lifetime; (2) a single input lifetime propagates to outputs; (3) &self/&mut self propagates to outputs. When these rules yield one answer, you write nothing.
  • Struct fields always need annotations: Elision only applies to function signatures. A struct that holds a reference must declare struct Foo<'a> { field: &'a T } — there is no single obvious input to elide from.
  • Ambiguous inputs force explicit annotations: fn f(a: &str, b: &str) -> &str is a compile error because rule 2 no longer applies — two lifetimes, two possible sources. Rust forces you to pick: fn f<'a>(a: &'a str, b: &str) -> &'a str.
  • Mental expansion: Reading elided code is easiest when you mentally restore the annotations: fn get_content(&self) -> &strfn get_content<'a>(&'a self) -> &'a str. This tells you the returned &str cannot outlive self.
  • When to Use Each Style

    Use elided lifetimes when: the function has a single input reference, or is a method where &self is the obvious donor — which covers the vast majority of real Rust code. Use explicit lifetimes when: there are multiple input references and the output could borrow from more than one, when a struct stores a reference, or when you want to document a non-obvious lifetime relationship for readers.

    Exercises

  • Write a function with two &str inputs and one &str output. Verify that an explicit lifetime annotation is required. Then add it.
  • Create a struct Parser<'a> { input: &'a str, pos: usize } and implement three methods that all return references — verify which ones need explicit annotations.
  • Write a function fn split_at_first_space(s: &str) -> (&str, &str) returning two slices of the input and verify the elision rules apply.
  • Open Source Repos