ExamplesBy LevelBy TopicLearning Paths
531 Intermediate

Lifetime Annotations: 'a Basics

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lifetime Annotations: 'a Basics" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Memory safety without a garbage collector requires the compiler to track how long references are valid. Key difference from OCaml: 1. **Compile

Tutorial

The Problem

Memory safety without a garbage collector requires the compiler to track how long references are valid. C and C++ leave this to the programmer, leading to use-after-free bugs — one of the most common sources of security vulnerabilities (CVEs). Rust solves this with lifetimes: explicit annotations that encode reference validity constraints in the type system. When a function returns a reference, the compiler needs to know which input the output borrows from, so it can reject code that would create a dangling pointer. Lifetime annotations are the mechanism for expressing this relationship.

🎯 Learning Outcomes

  • • What lifetime annotations express: constraints on how long references remain valid
  • • How 'a in fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str constrains the output
  • • The lifetime elision rules that let most functions omit explicit annotations
  • • How structs holding references require lifetime parameters: struct Excerpt<'a> { part: &'a str }
  • • Why lifetime annotations prevent use-after-free at compile time
  • Code Example

    // Explicit lifetime: output valid while both inputs valid
    pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() >= s2.len() { s1 } else { s2 }
    }
    
    // Elided lifetime: compiler infers input → output
    pub fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or("")
    }

    Key Differences

  • Compile-time vs runtime safety: Rust lifetime annotations catch dangling references at compile time with zero runtime overhead; OCaml's GC catches use-after-free at... never, because the GC keeps everything alive.
  • Annotation burden: Rust requires explicit lifetime annotations when elision rules do not apply; OCaml requires no annotations because ownership is not tracked.
  • Struct references: Rust struct Excerpt<'a> must annotate the lifetime of borrowed fields; OCaml records holding references need no annotation — the GC manages all values.
  • Output lifetime ambiguity: When a function has multiple reference inputs and one reference output, Rust requires the programmer to specify which input the output borrows from; OCaml has no such requirement.
  • OCaml Approach

    OCaml has no lifetime annotations — the garbage collector ensures referenced values remain alive as long as any reference exists. The equivalent of longer is:

    let longer s1 s2 = if String.length s1 >= String.length s2 then s1 else s2
    

    There is no annotation needed, and the GC prevents dangling references. However, this means OCaml programs cannot express "this reference is borrowed, not owned" at the type level.

    Full Source

    #![allow(clippy::all)]
    //! Lifetime Annotations: 'a Basics
    //!
    //! Explicit lifetime parameters expressing reference validity constraints.
    
    /// Return the longer of two string slices.
    /// 'a means: output valid as long as BOTH inputs are valid.
    pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() >= s2.len() {
            s1
        } else {
            s2
        }
    }
    
    /// First word of a string: output tied to input's lifetime.
    pub fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or("")
    }
    
    /// Two different lifetimes: x and y may have different scopes.
    pub fn pick_first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
        x // output tied to 'a only
    }
    
    /// Struct holding a reference — must annotate lifetime.
    #[derive(Debug)]
    pub struct Excerpt<'a> {
        pub part: &'a str,
    }
    
    impl<'a> Excerpt<'a> {
        pub fn new(text: &'a str) -> Self {
            Excerpt { part: text }
        }
    
        pub fn part(&self) -> &str {
            self.part
        }
    }
    
    /// Function returning a reference to one of its inputs.
    pub fn min_by_len<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() <= b.len() {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longer() {
            let s1 = "hello";
            let s2 = "hi";
            assert_eq!(longer(s1, s2), "hello");
        }
    
        #[test]
        fn test_longer_equal() {
            let s1 = "abc";
            let s2 = "xyz";
            assert_eq!(longer(s1, s2), "abc"); // first wins on tie
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
            assert_eq!(first_word(""), "");
        }
    
        #[test]
        fn test_pick_first() {
            let x = "first";
            let y = "second";
            assert_eq!(pick_first(x, y), "first");
        }
    
        #[test]
        fn test_excerpt() {
            let text = String::from("Call me Ishmael.");
            let excerpt = Excerpt::new(&text[..7]);
            assert_eq!(excerpt.part(), "Call me");
        }
    
        #[test]
        fn test_min_by_len() {
            assert_eq!(min_by_len("hi", "hello"), "hi");
            assert_eq!(min_by_len("abc", "ab"), "ab");
        }
    
        #[test]
        fn test_lifetime_scope() {
            let result;
            {
                let s1 = String::from("long string");
                let s2 = String::from("short");
                result = longer(&s1, &s2);
                assert_eq!(result, "long string");
            }
            // result is no longer valid here because s1, s2 are dropped
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longer() {
            let s1 = "hello";
            let s2 = "hi";
            assert_eq!(longer(s1, s2), "hello");
        }
    
        #[test]
        fn test_longer_equal() {
            let s1 = "abc";
            let s2 = "xyz";
            assert_eq!(longer(s1, s2), "abc"); // first wins on tie
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
            assert_eq!(first_word(""), "");
        }
    
        #[test]
        fn test_pick_first() {
            let x = "first";
            let y = "second";
            assert_eq!(pick_first(x, y), "first");
        }
    
        #[test]
        fn test_excerpt() {
            let text = String::from("Call me Ishmael.");
            let excerpt = Excerpt::new(&text[..7]);
            assert_eq!(excerpt.part(), "Call me");
        }
    
        #[test]
        fn test_min_by_len() {
            assert_eq!(min_by_len("hi", "hello"), "hi");
            assert_eq!(min_by_len("abc", "ab"), "ab");
        }
    
        #[test]
        fn test_lifetime_scope() {
            let result;
            {
                let s1 = String::from("long string");
                let s2 = String::from("short");
                result = longer(&s1, &s2);
                assert_eq!(result, "long string");
            }
            // result is no longer valid here because s1, s2 are dropped
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetime Basics

    OCaml

    (* No lifetimes needed — GC manages memory *)
    let longer s1 s2 =
      if String.length s1 >= String.length s2 then s1 else s2
    
    let first_word s =
      match String.split_on_char ' ' s with
      | [] -> ""
      | w :: _ -> w
    

    Rust

    // Explicit lifetime: output valid while both inputs valid
    pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() >= s2.len() { s1 } else { s2 }
    }
    
    // Elided lifetime: compiler infers input → output
    pub fn first_word(s: &str) -> &str {
        s.split_whitespace().next().unwrap_or("")
    }
    

    Key Differences

  • OCaml: GC tracks object reachability, no explicit lifetimes
  • Rust: Lifetimes express "how long is this reference valid?"
  • Rust: 'a is a lifetime parameter, like a generic type
  • Rust: Output lifetime tied to input lifetimes
  • Both: Prevent use-after-free, different mechanisms
  • Exercises

  • Longest word: Write fn longest_word<'a>(sentence: &'a str) -> &'a str that returns a slice pointing into the original string for the longest whitespace-separated word.
  • Pair of borrows: Implement struct Pair<'a, 'b> { first: &'a str, second: &'b str } and a method fn shorter(&self) -> &str — figure out what lifetime the return type needs.
  • Nested lifetime: Write a function fn inner_word<'a>(outer: &'a str, _separator: &str) -> &'a str that returns the substring before the first comma, tied only to outer's lifetime.
  • Open Source Repos