ExamplesBy LevelBy TopicLearning Paths
533 Intermediate

Lifetimes in Structs

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lifetimes in Structs" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Most structs own their data outright. Key difference from OCaml: 1. **Zero

Tutorial

The Problem

Most structs own their data outright. But some structs are intentionally views or windows into existing data — text highlights, tokenizer state, zero-copy parsers, iterator adapters. These structs hold references rather than owned values, which means their validity is tied to the lifetime of the data they reference. Rust's lifetime parameters on structs make this relationship explicit in the type, preventing a view struct from outliving its source data. This pattern is essential for zero-copy parsing (nom, winnow), text processing, and embedded data structures.

🎯 Learning Outcomes

  • • Why structs with reference fields require lifetime parameters: struct Highlight<'a>
  • • How Highlight<'a> borrows from a source string and cannot outlive it
  • • How iterators holding a reference (struct Words<'a>) work with lifetime-annotated Iterator impls
  • • How multiple reference fields in a struct can share or have independent lifetimes
  • • Where lifetime-annotated structs appear: parsers (nom), zero-copy deserializers (serde)
  • Code Example

    // Struct borrowing from external string needs 'a
    #[derive(Debug)]
    pub struct Highlight<'a> {
        pub text: &'a str,  // borrows from source
        pub start: usize,
        pub end: usize,
    }
    
    impl<'a> Highlight<'a> {
        pub fn new(source: &'a str, start: usize, end: usize) -> Option<Self> {
            Some(Highlight { text: &source[start..end], start, end })
        }
    }

    Key Differences

  • Zero-copy slices: Rust &'a str in a struct is a true zero-copy view into the source; OCaml String.sub copies the substring — zero-copy requires lower-level types.
  • Lifetime annotation: Rust requires <'a> on struct definitions holding references; OCaml records hold GC-managed values with no lifetime annotation needed.
  • Iterator lifetimes: Rust impl Iterator on a struct with 'a yields references tied to the source; OCaml iterators return owned values by default.
  • Compile-time vs GC: Rust prevents use-after-free at compile time by tracking lifetimes; OCaml prevents it at runtime via garbage collection.
  • OCaml Approach

    OCaml string views use string * int * int tuples or a dedicated Bigarray slice. Since OCaml strings are immutable and GC-managed, holding a slice is safe with no annotations:

    type highlight = { text: string; start: int; end_: int }
    let make_highlight source start end_ =
      if end_ <= String.length source then Some { text = String.sub source start (end_ - start); start; end_ }
      else None
    

    Note: String.sub copies — zero-copy substring views require Bytes or Bigarray.

    Full Source

    #![allow(clippy::all)]
    //! Lifetimes in Structs
    //!
    //! Struct fields that are references require lifetime annotations.
    
    /// A highlight into a larger text — borrows from the source string.
    #[derive(Debug, Clone)]
    pub struct Highlight<'a> {
        pub text: &'a str,
        pub start: usize,
        pub end: usize,
    }
    
    impl<'a> Highlight<'a> {
        pub fn new(source: &'a str, start: usize, end: usize) -> Option<Self> {
            if end <= source.len() && start <= end {
                Some(Highlight {
                    text: &source[start..end],
                    start,
                    end,
                })
            } else {
                None
            }
        }
    
        pub fn text(&self) -> &str {
            self.text
        }
    }
    
    /// Iterator that borrows from source.
    pub struct Words<'a> {
        source: &'a str,
        position: usize,
    }
    
    impl<'a> Words<'a> {
        pub fn new(source: &'a str) -> Self {
            Words {
                source,
                position: 0,
            }
        }
    }
    
    impl<'a> Iterator for Words<'a> {
        type Item = &'a str;
    
        fn next(&mut self) -> Option<Self::Item> {
            let remaining = &self.source[self.position..];
            let trimmed = remaining.trim_start();
            if trimmed.is_empty() {
                return None;
            }
            self.position = self.source.len() - trimmed.len();
    
            let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
            self.position += end;
            Some(&trimmed[..end])
        }
    }
    
    /// Config that borrows from environment.
    #[derive(Debug)]
    pub struct Config<'a> {
        pub name: &'a str,
        pub values: Vec<&'a str>,
    }
    
    impl<'a> Config<'a> {
        pub fn new(name: &'a str) -> Self {
            Config {
                name,
                values: Vec::new(),
            }
        }
    
        pub fn add_value(&mut self, value: &'a str) {
            self.values.push(value);
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_highlight_creation() {
            let text = "Hello, World!";
            let highlight = Highlight::new(text, 0, 5).unwrap();
            assert_eq!(highlight.text(), "Hello");
            assert_eq!(highlight.start, 0);
            assert_eq!(highlight.end, 5);
        }
    
        #[test]
        fn test_highlight_invalid() {
            let text = "short";
            assert!(Highlight::new(text, 0, 100).is_none());
            assert!(Highlight::new(text, 5, 3).is_none());
        }
    
        #[test]
        fn test_words_iterator() {
            let text = "hello world rust";
            let words: Vec<&str> = Words::new(text).collect();
            assert_eq!(words, vec!["hello", "world", "rust"]);
        }
    
        #[test]
        fn test_words_empty() {
            let words: Vec<&str> = Words::new("").collect();
            assert!(words.is_empty());
        }
    
        #[test]
        fn test_config() {
            let name = "my_config";
            let mut config = Config::new(name);
            config.add_value("value1");
            config.add_value("value2");
    
            assert_eq!(config.name, "my_config");
            assert_eq!(config.values.len(), 2);
        }
    
        #[test]
        fn test_struct_lifetime_scope() {
            let highlight;
            {
                let text = String::from("temporary text");
                highlight = Highlight::new(&text, 0, 9);
                assert_eq!(highlight.as_ref().unwrap().text(), "temporary");
            }
            // highlight is now invalid because text is dropped
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_highlight_creation() {
            let text = "Hello, World!";
            let highlight = Highlight::new(text, 0, 5).unwrap();
            assert_eq!(highlight.text(), "Hello");
            assert_eq!(highlight.start, 0);
            assert_eq!(highlight.end, 5);
        }
    
        #[test]
        fn test_highlight_invalid() {
            let text = "short";
            assert!(Highlight::new(text, 0, 100).is_none());
            assert!(Highlight::new(text, 5, 3).is_none());
        }
    
        #[test]
        fn test_words_iterator() {
            let text = "hello world rust";
            let words: Vec<&str> = Words::new(text).collect();
            assert_eq!(words, vec!["hello", "world", "rust"]);
        }
    
        #[test]
        fn test_words_empty() {
            let words: Vec<&str> = Words::new("").collect();
            assert!(words.is_empty());
        }
    
        #[test]
        fn test_config() {
            let name = "my_config";
            let mut config = Config::new(name);
            config.add_value("value1");
            config.add_value("value2");
    
            assert_eq!(config.name, "my_config");
            assert_eq!(config.values.len(), 2);
        }
    
        #[test]
        fn test_struct_lifetime_scope() {
            let highlight;
            {
                let text = String::from("temporary text");
                highlight = Highlight::new(&text, 0, 9);
                assert_eq!(highlight.as_ref().unwrap().text(), "temporary");
            }
            // highlight is now invalid because text is dropped
        }
    }

    Deep Comparison

    OCaml vs Rust: Struct Lifetimes

    OCaml

    (* GC handles string ownership — no lifetime annotation *)
    type highlight = {
      text: string;
      start: int;
      end_pos: int;
    }
    
    let make_highlight source start end_pos =
      { text = String.sub source start (end_pos - start);
        start; end_pos }
    

    Rust

    // Struct borrowing from external string needs 'a
    #[derive(Debug)]
    pub struct Highlight<'a> {
        pub text: &'a str,  // borrows from source
        pub start: usize,
        pub end: usize,
    }
    
    impl<'a> Highlight<'a> {
        pub fn new(source: &'a str, start: usize, end: usize) -> Option<Self> {
            Some(Highlight { text: &source[start..end], start, end })
        }
    }
    

    Key Differences

  • OCaml: Strings are values, copied or shared via GC
  • Rust: &str borrows, struct must track borrow lifetime
  • Rust: 'a parameter says "valid as long as source"
  • Rust: Struct cannot outlive borrowed data
  • Both: Enable zero-copy views into larger strings
  • Exercises

  • Line iterator: Implement struct Lines<'a> { source: &'a str, pos: usize } with Iterator<Item = &'a str> that yields each line (split on '\n') as a zero-copy slice.
  • Token view: Write struct Token<'a> { kind: TokenKind, text: &'a str } where TokenKind is an enum and text borrows from the input source string.
  • Multi-source struct: Implement struct Merge<'a, 'b> { left: &'a [i32], right: &'b [i32], pos_left: usize, pos_right: usize } as a merge-sort iterator yielding i32 values in sorted order.
  • Open Source Repos