ExamplesBy LevelBy TopicLearning Paths
105 Intermediate

105-lifetime-basics — Lifetime Basics

Functional Programming

Tutorial

The Problem

Returning a reference from a function is safe only if the referenced data outlives the reference. In C, returning a pointer to a local variable is undefined behavior — the data is destroyed when the function returns. Rust's lifetime system prevents this at compile time by tracking how long each reference is valid.

Lifetime annotations ('a) do not change runtime behavior — they are purely compile-time metadata that the borrow checker uses to verify that no reference outlives its data.

🎯 Learning Outcomes

  • • Understand lifetime annotations as relationships between input and output reference lifetimes
  • • Read fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str and explain what 'a means
  • • Annotate structs that hold references
  • • Understand the three lifetime elision rules and when annotations are omitted
  • • Know the 'static lifetime and when it applies
  • Code Example

    #![allow(clippy::all)]
    // 105: Lifetime Basics
    // Lifetime annotations tell the compiler how long references live
    
    // Approach 1: Lifetime in function signature
    // 'a means: the returned reference lives as long as the inputs
    fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() >= s2.len() {
            s1
        } else {
            s2
        }
    }
    
    fn first_element<'a>(v: &'a [i32]) -> Option<&'a i32> {
        v.first()
    }
    
    // Approach 2: Lifetime in struct
    struct Important<'a> {
        content: &'a str,
    }
    
    impl<'a> Important<'a> {
        fn new(content: &'a str) -> Self {
            Important { content }
        }
    
        fn content(&self) -> &str {
            self.content
        }
    }
    
    // Approach 3: Multiple lifetimes
    fn first_word<'a>(s: &'a str) -> &'a str {
        let bytes = s.as_bytes();
        for (i, &byte) in bytes.iter().enumerate() {
            if byte == b' ' {
                return &s[..i];
            }
        }
        s
    }
    
    // This would NOT compile — dangling reference:
    // fn dangling() -> &str {
    //     let s = String::from("hello");
    //     &s // ERROR: s dropped here, reference would dangle
    // }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longest() {
            assert_eq!(longest("hello", "hi"), "hello");
            assert_eq!(longest("a", "bb"), "bb");
        }
    
        #[test]
        fn test_first_element() {
            assert_eq!(first_element(&[1, 2, 3]), Some(&1));
            assert_eq!(first_element(&[]), None);
        }
    
        #[test]
        fn test_important() {
            let msg = Important::new("test");
            assert_eq!(msg.content(), "test");
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }

    Key Differences

  • Compile time vs runtime: Rust's lifetimes prevent dangling pointers at compile time with zero runtime overhead; OCaml's GC prevents them at runtime.
  • Annotation burden: Rust requires explicit lifetime annotations when the compiler cannot infer them; OCaml has none.
  • **'static lifetime**: Rust's &'static str lives for the entire program duration (string literals); OCaml has no equivalent concept because the GC handles all lifetimes uniformly.
  • Relationships: Rust lifetimes express relationships between references (output borrows from input); OCaml's GC makes these relationships implicit.
  • OCaml Approach

    OCaml has no lifetime system. The GC ensures referenced data lives at least as long as any reference to it:

    let longest s1 s2 =
      if String.length s1 >= String.length s2 then s1 else s2
    (* Both s1 and s2 are GC-managed; the returned reference is always valid *)
    

    A struct holding a reference is just a record with a string field — the GC tracks all references and prevents dangling pointers at runtime.

    Full Source

    #![allow(clippy::all)]
    // 105: Lifetime Basics
    // Lifetime annotations tell the compiler how long references live
    
    // Approach 1: Lifetime in function signature
    // 'a means: the returned reference lives as long as the inputs
    fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() >= s2.len() {
            s1
        } else {
            s2
        }
    }
    
    fn first_element<'a>(v: &'a [i32]) -> Option<&'a i32> {
        v.first()
    }
    
    // Approach 2: Lifetime in struct
    struct Important<'a> {
        content: &'a str,
    }
    
    impl<'a> Important<'a> {
        fn new(content: &'a str) -> Self {
            Important { content }
        }
    
        fn content(&self) -> &str {
            self.content
        }
    }
    
    // Approach 3: Multiple lifetimes
    fn first_word<'a>(s: &'a str) -> &'a str {
        let bytes = s.as_bytes();
        for (i, &byte) in bytes.iter().enumerate() {
            if byte == b' ' {
                return &s[..i];
            }
        }
        s
    }
    
    // This would NOT compile — dangling reference:
    // fn dangling() -> &str {
    //     let s = String::from("hello");
    //     &s // ERROR: s dropped here, reference would dangle
    // }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longest() {
            assert_eq!(longest("hello", "hi"), "hello");
            assert_eq!(longest("a", "bb"), "bb");
        }
    
        #[test]
        fn test_first_element() {
            assert_eq!(first_element(&[1, 2, 3]), Some(&1));
            assert_eq!(first_element(&[]), None);
        }
    
        #[test]
        fn test_important() {
            let msg = Important::new("test");
            assert_eq!(msg.content(), "test");
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longest() {
            assert_eq!(longest("hello", "hi"), "hello");
            assert_eq!(longest("a", "bb"), "bb");
        }
    
        #[test]
        fn test_first_element() {
            assert_eq!(first_element(&[1, 2, 3]), Some(&1));
            assert_eq!(first_element(&[]), None);
        }
    
        #[test]
        fn test_important() {
            let msg = Important::new("test");
            assert_eq!(msg.content(), "test");
        }
    
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }

    Deep Comparison

    Core Insight

    Lifetimes tell the compiler how long references are valid — preventing dangling references at compile time

    OCaml Approach

  • • See example.ml for implementation
  • Rust Approach

  • • See example.rs for implementation
  • Comparison Table

    FeatureOCamlRust
    Seeexample.mlexample.rs

    Exercises

  • Write a function longest_word<'a>(sentence: &'a str) -> &'a str that returns the longest word from a sentence as a borrowed slice.
  • Create a Cache<'a> { data: &'a [i32], computed: Vec<i32> } struct and implement a method that borrows from data and populates computed.
  • Demonstrate the lifetime error when trying to return a reference to a local variable and explain the exact compiler message.
  • Open Source Repos