ExamplesBy LevelBy TopicLearning Paths
532 Intermediate

Multiple Lifetime Parameters

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Multiple Lifetime Parameters" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Most introductory lifetime examples use a single `'a` for all borrows. Key difference from OCaml: 1. **Lifetime independence**: Rust requires separate `'a` and `'b` when two references can have different scopes; OCaml has no such distinction — the GC ensures both are valid as long as needed.

Tutorial

The Problem

Most introductory lifetime examples use a single 'a for all borrows. But real code often has references with genuinely independent lifetimes — a function that reads from one buffer and writes to another, a struct holding a reader and a writer that may live for different durations. Using a single lifetime in these cases would over-constrain the API: callers would need to keep all referenced data alive for the same duration. Multiple independent lifetime parameters express the true dependency relationships and give callers maximum flexibility.

🎯 Learning Outcomes

  • • Why first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str uses two lifetimes instead of one
  • • How struct Pair<'a, 'b> with independent field lifetimes enables flexible use
  • • How impl<'a, 'b> Pair<'a, 'b> methods can return references tied to specific fields
  • • How Context<'r, 'w> models independent reader ('r) and writer ('w) lifetimes
  • • When to use lifetime subtyping ('long: 'short) to express ordering constraints
  • Code Example

    // Independent lifetimes for different fields
    pub struct Pair<'a, 'b> {
        pub first: &'a str,
        pub second: &'b str,
    }
    
    // Output tied to first input only
    pub fn first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
        x  // 'b can be shorter than 'a
    }

    Key Differences

  • Lifetime independence: Rust requires separate 'a and 'b when two references can have different scopes; OCaml has no such distinction — the GC ensures both are valid as long as needed.
  • Return source tracing: Rust methods that return references must specify which field they return from via the lifetime; OCaml methods return values with no annotation on where they came from.
  • API flexibility: Two-lifetime Rust APIs are strictly more flexible for callers than single-lifetime APIs; OCaml APIs are uniformly flexible because all values are GC-managed.
  • Lifetime subtyping: Rust 'long: 'short expresses outlives relationships as a compile-time constraint; OCaml has no equivalent — the runtime guarantee is unconditional.
  • OCaml Approach

    OCaml has no lifetime parameters — all borrows are managed by the GC. A record with two string references needs no annotation:

    type pair = { first: string; second: string }
    let get_first p = p.first   (* no lifetime annotation needed *)
    let get_second p = p.second
    

    Multiple independent reference lifetimes are a Rust-specific concept; OCaml programs never express or reason about them.

    Full Source

    #![allow(clippy::all)]
    //! Multiple Lifetime Parameters
    //!
    //! Independent lifetimes for inputs with different validity scopes.
    
    /// Output tied to x only — y can have shorter lifetime.
    pub fn first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
        x
    }
    
    /// Struct with two independent borrowed fields.
    #[derive(Debug)]
    pub struct Pair<'a, 'b> {
        pub first: &'a str,
        pub second: &'b str,
    }
    
    impl<'a, 'b> Pair<'a, 'b> {
        pub fn new(first: &'a str, second: &'b str) -> Self {
            Pair { first, second }
        }
    
        /// Returns from first — tied to 'a.
        pub fn get_first(&self) -> &'a str {
            self.first
        }
    
        /// Returns from second — tied to 'b.
        pub fn get_second(&self) -> &'b str {
            self.second
        }
    }
    
    /// Context with reader and writer — independent lifetimes.
    pub struct Context<'r, 'w> {
        reader: &'r str,
        writer: &'w mut String,
    }
    
    impl<'r, 'w> Context<'r, 'w> {
        pub fn new(reader: &'r str, writer: &'w mut String) -> Self {
            Context { reader, writer }
        }
    
        pub fn read(&self) -> &'r str {
            self.reader
        }
    
        pub fn write(&mut self, s: &str) {
            self.writer.push_str(s);
        }
    }
    
    /// Three different lifetimes.
    pub fn select<'a, 'b, 'c>(a: &'a str, _b: &'b str, _c: &'c str, choice: usize) -> &'a str {
        // Can only return 'a since that's what we promised
        match choice {
            _ => a,
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_first_of() {
            let x = "first";
            {
                let y = String::from("second");
                let result = first_of(x, &y);
                assert_eq!(result, "first");
            }
            // x is still valid, y is dropped
        }
    
        #[test]
        fn test_pair_independent() {
            let first = String::from("hello");
            let second = String::from("world");
            let pair = Pair::new(&first, &second);
            assert_eq!(pair.get_first(), "hello");
            assert_eq!(pair.get_second(), "world");
            // Both references tied to their respective lifetimes
        }
    
        #[test]
        fn test_pair_same_lifetime() {
            let s1 = "hello";
            let s2 = "world";
            let pair = Pair::new(s1, s2);
            assert_eq!(pair.get_first(), "hello");
            assert_eq!(pair.get_second(), "world");
        }
    
        #[test]
        fn test_context() {
            let input = "read this";
            let mut output = String::new();
            let mut ctx = Context::new(input, &mut output);
    
            assert_eq!(ctx.read(), "read this");
            ctx.write("wrote this");
            assert_eq!(output, "wrote this");
        }
    
        #[test]
        fn test_select() {
            let a = "a";
            let b = "b";
            let c = "c";
            assert_eq!(select(a, b, c, 0), "a");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_first_of() {
            let x = "first";
            {
                let y = String::from("second");
                let result = first_of(x, &y);
                assert_eq!(result, "first");
            }
            // x is still valid, y is dropped
        }
    
        #[test]
        fn test_pair_independent() {
            let first = String::from("hello");
            let second = String::from("world");
            let pair = Pair::new(&first, &second);
            assert_eq!(pair.get_first(), "hello");
            assert_eq!(pair.get_second(), "world");
            // Both references tied to their respective lifetimes
        }
    
        #[test]
        fn test_pair_same_lifetime() {
            let s1 = "hello";
            let s2 = "world";
            let pair = Pair::new(s1, s2);
            assert_eq!(pair.get_first(), "hello");
            assert_eq!(pair.get_second(), "world");
        }
    
        #[test]
        fn test_context() {
            let input = "read this";
            let mut output = String::new();
            let mut ctx = Context::new(input, &mut output);
    
            assert_eq!(ctx.read(), "read this");
            ctx.write("wrote this");
            assert_eq!(output, "wrote this");
        }
    
        #[test]
        fn test_select() {
            let a = "a";
            let b = "b";
            let c = "c";
            assert_eq!(select(a, b, c, 0), "a");
        }
    }

    Deep Comparison

    OCaml vs Rust: Multiple Lifetimes

    OCaml

    (* No concept of multiple lifetimes — GC handles all *)
    type pair = { first: string; second: string }
    
    let first_of x _y = x
    let make_pair first second = { first; second }
    

    Rust

    // Independent lifetimes for different fields
    pub struct Pair<'a, 'b> {
        pub first: &'a str,
        pub second: &'b str,
    }
    
    // Output tied to first input only
    pub fn first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
        x  // 'b can be shorter than 'a
    }
    

    Key Differences

  • OCaml: Single memory model, GC tracks all references
  • Rust: Multiple lifetimes express independent validity
  • Rust: 'a and 'b can have different scopes
  • Rust: Return type specifies which lifetime applies
  • Rust: Enables borrowing from multiple sources safely
  • Exercises

  • Three-lifetime function: Write fn combine<'a, 'b, 'c>(x: &'a str, y: &'b str, sep: &'c str) -> String where the output owns its data (not tied to any input lifetime).
  • Independent reader/writer: Implement struct Log<'data, 'label> { entries: &'data [String], prefix: &'label str } with a method that formats entries using the prefix, returning an owned String.
  • Lifetime subtyping demo: Write fn coerce<'long: 'short, 'short>(x: &'long str) -> &'short str { x } and explain in a comment why this compiles — 'long can be used where 'short is expected.
  • Open Source Repos