Multiple Lifetime Parameters
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
first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str uses two lifetimes instead of onestruct Pair<'a, 'b> with independent field lifetimes enables flexible useimpl<'a, 'b> Pair<'a, 'b> methods can return references tied to specific fieldsContext<'r, 'w> models independent reader ('r) and writer ('w) lifetimes'long: 'short) to express ordering constraintsCode 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
'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.'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");
}
}#[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
Exercises
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).struct Log<'data, 'label> { entries: &'data [String], prefix: &'label str } with a method that formats entries using the prefix, returning an owned String.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.