Lifetimes in Structs
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
struct Highlight<'a>Highlight<'a> borrows from a source string and cannot outlive itstruct Words<'a>) work with lifetime-annotated Iterator implsCode 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
&'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.<'a> on struct definitions holding references; OCaml records hold GC-managed values with no lifetime annotation needed.impl Iterator on a struct with 'a yields references tied to the source; OCaml iterators return owned values by default.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
}
}#[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
Exercises
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.struct Token<'a> { kind: TokenKind, text: &'a str } where TokenKind is an enum and text borrows from the input source string.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.