ExamplesBy LevelBy TopicLearning Paths
557 Intermediate

Output Lifetime Patterns

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Output Lifetime Patterns" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Every function returning a reference must express where that reference comes from — the output lifetime. Key difference from OCaml: 1. **Output source tracing**: Rust requires the programmer to specify which input a returned reference borrows from; OCaml returns owned or GC

Tutorial

The Problem

Every function returning a reference must express where that reference comes from — the output lifetime. In simple cases, elision handles this. In complex cases, understanding which input the output borrows from is essential for correctness. Three distinct patterns cover most use cases: (1) output tied to a single input, (2) output tied to the shortest common lifetime of multiple inputs, (3) output with a lifetime independent of inputs ('static). Getting the output lifetime wrong causes either compile errors or unnecessarily restrictive APIs.

🎯 Learning Outcomes

  • • How first_char(s: &str) -> Option<&str> ties the output to the single input via elision
  • • How common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str uses one lifetime for both inputs
  • • How static_str(_s: &str) -> &'static str returns data independent of the input
  • • How struct methods can return &self-lifetime vs stored-data-lifetime references
  • • When choosing the correct output lifetime affects API ergonomics
  • Code Example

    #![allow(clippy::all)]
    //! Output Lifetime Patterns
    //!
    //! How output lifetimes relate to inputs.
    
    /// Output tied to single input.
    pub fn first_char(s: &str) -> Option<&str> {
        s.chars().next().map(|c| &s[..c.len_utf8()])
    }
    
    /// Output tied to shortest input.
    pub fn common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str {
        let len = a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count();
        &a[..a.chars().take(len).map(|c| c.len_utf8()).sum()]
    }
    
    /// Output lifetime independent of input.
    pub fn static_str(_s: &str) -> &'static str {
        "static"
    }
    
    /// Struct returning references to its data.
    pub struct Container {
        items: Vec<String>,
    }
    
    impl Container {
        pub fn new() -> Self {
            Container { items: Vec::new() }
        }
    
        pub fn add(&mut self, s: &str) {
            self.items.push(s.to_string());
        }
    
        pub fn get(&self, idx: usize) -> Option<&str> {
            self.items.get(idx).map(|s| s.as_str())
        }
    }
    
    impl Default for Container {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_first_char() {
            assert_eq!(first_char("hello"), Some("h"));
            assert_eq!(first_char(""), None);
        }
    
        #[test]
        fn test_common_prefix() {
            assert_eq!(common_prefix("hello", "help"), "hel");
            assert_eq!(common_prefix("abc", "xyz"), "");
        }
    
        #[test]
        fn test_static_str() {
            let s = String::from("temporary");
            let result = static_str(&s);
            assert_eq!(result, "static");
        }
    
        #[test]
        fn test_container() {
            let mut c = Container::new();
            c.add("hello");
            c.add("world");
            assert_eq!(c.get(0), Some("hello"));
            assert_eq!(c.get(1), Some("world"));
        }
    }

    Key Differences

  • Output source tracing: Rust requires the programmer to specify which input a returned reference borrows from; OCaml returns owned or GC-managed values with no source annotation needed.
  • Performance: Rust first_char returns a &str slice — zero allocation; OCaml String.sub copies the character into a new string.
  • Independent output: Rust -> &'static str is a common optimization for returning compile-time constants; OCaml constants are also zero-allocation but through GC interning.
  • Error source: When output lifetime is wrong in Rust, the compiler reports "does not live long enough" at the use site; OCaml never reports lifetime errors.
  • OCaml Approach

    OCaml functions return GC-managed values — there are no output lifetimes. The equivalent functions are:

    let first_char s = if String.length s = 0 then None else Some (String.sub s 0 1)
    let common_prefix a b = (* find and return common prefix as new string *)
    let static_str _ = "static"
    

    All returned values are GC-managed; no lifetime annotation describes where they came from.

    Full Source

    #![allow(clippy::all)]
    //! Output Lifetime Patterns
    //!
    //! How output lifetimes relate to inputs.
    
    /// Output tied to single input.
    pub fn first_char(s: &str) -> Option<&str> {
        s.chars().next().map(|c| &s[..c.len_utf8()])
    }
    
    /// Output tied to shortest input.
    pub fn common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str {
        let len = a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count();
        &a[..a.chars().take(len).map(|c| c.len_utf8()).sum()]
    }
    
    /// Output lifetime independent of input.
    pub fn static_str(_s: &str) -> &'static str {
        "static"
    }
    
    /// Struct returning references to its data.
    pub struct Container {
        items: Vec<String>,
    }
    
    impl Container {
        pub fn new() -> Self {
            Container { items: Vec::new() }
        }
    
        pub fn add(&mut self, s: &str) {
            self.items.push(s.to_string());
        }
    
        pub fn get(&self, idx: usize) -> Option<&str> {
            self.items.get(idx).map(|s| s.as_str())
        }
    }
    
    impl Default for Container {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_first_char() {
            assert_eq!(first_char("hello"), Some("h"));
            assert_eq!(first_char(""), None);
        }
    
        #[test]
        fn test_common_prefix() {
            assert_eq!(common_prefix("hello", "help"), "hel");
            assert_eq!(common_prefix("abc", "xyz"), "");
        }
    
        #[test]
        fn test_static_str() {
            let s = String::from("temporary");
            let result = static_str(&s);
            assert_eq!(result, "static");
        }
    
        #[test]
        fn test_container() {
            let mut c = Container::new();
            c.add("hello");
            c.add("world");
            assert_eq!(c.get(0), Some("hello"));
            assert_eq!(c.get(1), Some("world"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_first_char() {
            assert_eq!(first_char("hello"), Some("h"));
            assert_eq!(first_char(""), None);
        }
    
        #[test]
        fn test_common_prefix() {
            assert_eq!(common_prefix("hello", "help"), "hel");
            assert_eq!(common_prefix("abc", "xyz"), "");
        }
    
        #[test]
        fn test_static_str() {
            let s = String::from("temporary");
            let result = static_str(&s);
            assert_eq!(result, "static");
        }
    
        #[test]
        fn test_container() {
            let mut c = Container::new();
            c.add("hello");
            c.add("world");
            assert_eq!(c.get(0), Some("hello"));
            assert_eq!(c.get(1), Some("world"));
        }
    }

    Deep Comparison

    OCaml vs Rust: lifetime output lifetime

    See example.rs and example.ml for implementations.

    Key Differences

  • OCaml uses garbage collection
  • Rust uses ownership and borrowing
  • Both support the core concept
  • Exercises

  • Multiple output lifetimes: Write fn split_first<'a>(s: &'a str, sep: char) -> (&'a str, &'a str) returning the part before and after the first occurrence of sep as zero-copy slices.
  • Independent static: Implement fn status_message(code: u16) -> &'static str using a match expression returning string literal branches — verify no heap allocation occurs.
  • Container iterator: Add a method fn iter(&self) -> impl Iterator<Item = &str> to Container and verify the returned iterator's lifetime is tied to &self.
  • Open Source Repos