ExamplesBy LevelBy TopicLearning Paths
541 Intermediate

Lifetime Elision Rules

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lifetime Elision Rules" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Writing explicit lifetime annotations on every function would make Rust code extremely verbose. Key difference from OCaml: 1. **Annotation reduction**: Rust's elision rules eliminate annotations in ~90% of practical cases; OCaml eliminates them in 100% of cases because the GC removes the need.

Tutorial

The Problem

Writing explicit lifetime annotations on every function would make Rust code extremely verbose. Lifetime elision rules were introduced to allow the compiler to infer annotations in the most common cases, making everyday code read cleanly. Three rules cover the vast majority of functions: (1) each input reference gets its own lifetime, (2) if there is exactly one input lifetime, it propagates to all output references, (3) if one of the inputs is &self or &mut self, its lifetime propagates to all output references. Understanding these rules explains when annotations are required and why.

🎯 Learning Outcomes

  • • The three elision rules that allow annotations to be omitted in common cases
  • • How fn strlen(s: &str) -> usize expands to fn strlen<'a>(s: &'a str) -> usize
  • • How fn first_word(s: &str) -> &str expands by Rule 2 (one input → output gets its lifetime)
  • • How fn remaining(&self) -> &str on a struct expands by Rule 3 (&self lifetime)
  • • When elision fails: multiple reference inputs with a reference output require explicit annotation
  • Code Example

    // Elision Rule 1: Each input gets own lifetime
    fn strlen(s: &str) -> usize  // &'a str implicitly
    
    // Elision Rule 2: One input → output gets same lifetime
    fn first_word(s: &str) -> &str  // &'a str → &'a str
    
    // Elision Rule 3: &self → output gets self's lifetime
    impl Parser { fn remaining(&self) -> &str }
    
    // Multiple inputs: explicit required
    fn longer<'a>(x: &'a str, y: &'a str) -> &'a str

    Key Differences

  • Annotation reduction: Rust's elision rules eliminate annotations in ~90% of practical cases; OCaml eliminates them in 100% of cases because the GC removes the need.
  • Rule transparency: Rust programmers must understand elision rules to read and write idiomatic code; OCaml programmers never encounter lifetime annotations.
  • Elision failure: When Rust elision fails (multiple input references, multiple output references), explicit annotations are required — a learning hurdle that OCaml completely avoids.
  • Correctness guarantee: Rust's elision rules are specified precisely — they are not guesses; the expanded form is provably equivalent; OCaml's implicit safety comes from GC correctness.
  • OCaml Approach

    OCaml has no lifetime elision because there are no lifetime annotations to elide. All functions operate on GC-managed values, and no annotation is ever required:

    let strlen s = String.length s
    let first_word s = match String.split_on_char ' ' s with w :: _ -> w | [] -> ""
    let longer x y = if String.length x >= String.length y then x else y
    

    Full Source

    #![allow(clippy::all)]
    //! Lifetime Elision Rules
    //!
    //! When and how Rust infers lifetimes automatically.
    
    /// Rule 1: Each input ref gets own lifetime.
    /// Elided: fn strlen(s: &str) -> usize
    /// Expanded: fn strlen<'a>(s: &'a str) -> usize
    pub fn strlen(s: &str) -> usize {
        s.len()
    }
    
    /// Rule 2: If one input lifetime, output gets it.
    /// Elided: fn first_word(s: &str) -> &str
    /// Expanded: fn first_word<'a>(s: &'a str) -> &'a str
    pub fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or("")
    }
    
    /// Rule 3: If &self or &mut self, output gets self's lifetime.
    pub struct Parser<'a> {
        input: &'a str,
    }
    
    impl<'a> Parser<'a> {
        /// Elided: fn remaining(&self) -> &str
        /// Expanded: fn remaining(&self) -> &'a str
        pub fn remaining(&self) -> &str {
            self.input
        }
    }
    
    /// Multiple inputs: cannot elide output lifetime.
    /// Must be explicit: fn longer<'a>(x: &'a str, y: &'a str) -> &'a str
    pub fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() >= y.len() {
            x
        } else {
            y
        }
    }
    
    /// No elision needed for non-reference returns.
    pub fn count_words(s: &str, _other: &str) -> usize {
        s.split_whitespace().count()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_strlen() {
            assert_eq!(strlen("hello"), 5);
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    
        #[test]
        fn test_parser_remaining() {
            let parser = Parser {
                input: "test input",
            };
            assert_eq!(parser.remaining(), "test input");
        }
    
        #[test]
        fn test_longer() {
            assert_eq!(longer("hi", "hello"), "hello");
        }
    
        #[test]
        fn test_count_words() {
            assert_eq!(count_words("a b c", "ignored"), 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_strlen() {
            assert_eq!(strlen("hello"), 5);
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    
        #[test]
        fn test_parser_remaining() {
            let parser = Parser {
                input: "test input",
            };
            assert_eq!(parser.remaining(), "test input");
        }
    
        #[test]
        fn test_longer() {
            assert_eq!(longer("hi", "hello"), "hello");
        }
    
        #[test]
        fn test_count_words() {
            assert_eq!(count_words("a b c", "ignored"), 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetime Elision

    OCaml

    (* No lifetime annotations ever needed *)
    let strlen s = String.length s
    let first_word s = List.hd (String.split_on_char ' ' s)
    

    Rust

    // Elision Rule 1: Each input gets own lifetime
    fn strlen(s: &str) -> usize  // &'a str implicitly
    
    // Elision Rule 2: One input → output gets same lifetime
    fn first_word(s: &str) -> &str  // &'a str → &'a str
    
    // Elision Rule 3: &self → output gets self's lifetime
    impl Parser { fn remaining(&self) -> &str }
    
    // Multiple inputs: explicit required
    fn longer<'a>(x: &'a str, y: &'a str) -> &'a str
    

    Key Differences

  • OCaml: No concept of lifetimes
  • Rust: Three elision rules reduce annotation burden
  • Rust: Compiler infers common patterns
  • Rust: Explicit when ambiguous (multiple inputs)
  • Both: Clean API, different mechanisms
  • Exercises

  • Manual expansion: Take the five functions in the source file and write out their fully-expanded forms with all lifetime annotations explicit — verify they produce identical behavior.
  • Elision limit: Write a function fn pick(cond: bool, a: &str, b: &str) -> &str — observe that elision fails here and add the correct annotation.
  • Method elision: Add a method fn peek(&self) -> char to Parser that returns the first character of self.input — verify elision applies Rule 3 correctly.
  • Open Source Repos