Output Lifetime Patterns
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
first_char(s: &str) -> Option<&str> ties the output to the single input via elisioncommon_prefix<'a>(a: &'a str, b: &'a str) -> &'a str uses one lifetime for both inputsstatic_str(_s: &str) -> &'static str returns data independent of the input&self-lifetime vs stored-data-lifetime referencesCode 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
first_char returns a &str slice — zero allocation; OCaml String.sub copies the character into a new string.-> &'static str is a common optimization for returning compile-time constants; OCaml constants are also zero-allocation but through GC interning.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"));
}
}#[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
Exercises
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.fn status_message(code: u16) -> &'static str using a match expression returning string literal branches — verify no heap allocation occurs.fn iter(&self) -> impl Iterator<Item = &str> to Container and verify the returned iterator's lifetime is tied to &self.