Input Lifetime Patterns
Tutorial Video
Text description (accessibility)
This video demonstrates the "Input Lifetime Patterns" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Input lifetimes determine how long the references passed to a function must remain valid. Key difference from OCaml: 1. **Input validity duration**: Rust input lifetimes enforce at compile time how long the caller must keep inputs alive; OCaml's GC extends lifetime automatically as needed.
Tutorial
The Problem
Input lifetimes determine how long the references passed to a function must remain valid. A function with one &str input has a simple input lifetime; a function with multiple &str inputs has multiple potentially independent input lifetimes. Getting input lifetimes right determines how long callers must keep their data alive. Over-constraining input lifetimes (requiring longer-lived data than necessary) makes APIs rigid; under-constraining them is a compile error. This example surveys the common input lifetime patterns systematically.
🎯 Learning Outcomes
first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str expresses independent input lifetimesProcessor<'input> ties the struct to an input buffer's lifetime at constructionCode Example
#![allow(clippy::all)]
//! Input Lifetime Patterns
//!
//! How input lifetimes constrain function signatures.
/// Single input lifetime propagates to output.
pub fn echo<'a>(s: &'a str) -> &'a str {
s
}
/// Multiple inputs with independent lifetimes.
pub fn first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str {
a
}
/// Input lifetime bounds struct.
pub struct Processor<'input> {
data: &'input str,
}
impl<'input> Processor<'input> {
pub fn new(data: &'input str) -> Self {
Processor { data }
}
pub fn process(&self) -> &'input str {
self.data.trim()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_echo() {
assert_eq!(echo("hello"), "hello");
}
#[test]
fn test_first() {
assert_eq!(first("a", "b"), "a");
}
#[test]
fn test_processor() {
let p = Processor::new(" test ");
assert_eq!(p.process(), "test");
}
}Key Differences
<'a, 'b> allows two inputs to have different scopes — one can go out of scope before the other; OCaml treats all GC-managed inputs uniformly.Processor<'input> cannot outlive its input data; OCaml's processor keeps its data alive as long as the processor exists.OCaml Approach
OCaml input "lifetimes" are managed by the GC — the compiler does not track how long input data must remain valid. Functions that store references to inputs simply keep them alive through GC references:
type 'a processor = { data: 'a }
let process p = String.trim p.data (* data kept alive by GC *)
Full Source
#![allow(clippy::all)]
//! Input Lifetime Patterns
//!
//! How input lifetimes constrain function signatures.
/// Single input lifetime propagates to output.
pub fn echo<'a>(s: &'a str) -> &'a str {
s
}
/// Multiple inputs with independent lifetimes.
pub fn first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str {
a
}
/// Input lifetime bounds struct.
pub struct Processor<'input> {
data: &'input str,
}
impl<'input> Processor<'input> {
pub fn new(data: &'input str) -> Self {
Processor { data }
}
pub fn process(&self) -> &'input str {
self.data.trim()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_echo() {
assert_eq!(echo("hello"), "hello");
}
#[test]
fn test_first() {
assert_eq!(first("a", "b"), "a");
}
#[test]
fn test_processor() {
let p = Processor::new(" test ");
assert_eq!(p.process(), "test");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_echo() {
assert_eq!(echo("hello"), "hello");
}
#[test]
fn test_first() {
assert_eq!(first("a", "b"), "a");
}
#[test]
fn test_processor() {
let p = Processor::new(" test ");
assert_eq!(p.process(), "test");
}
}
Deep Comparison
OCaml vs Rust: lifetime input lifetime
See example.rs and example.ml for implementations.
Exercises
fn join3<'a>(sep: &str, a: &'a str, b: &'a str) -> String — note that sep does not appear in the output, so it does not need a named lifetime.struct Parser<'src> { input: &'src str, pos: usize } where the 'src lifetime is set at construction and next<'parser>(&'parser mut self) -> Option<&'src str> returns slices of the original input.fn with_prefix<'a>(s: &'a str, prefix: Option<&str>) -> String that prepends prefix to s if present — observe that prefix needs no named lifetime since it does not appear in the output.