Lifetime Elision Rules
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lifetime Elision Rules" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Writing explicit lifetime annotations on every function would make Rust code extremely verbose. Key difference from OCaml: 1. **Annotation reduction**: Rust's elision rules eliminate annotations in ~90% of practical cases; OCaml eliminates them in 100% of cases because the GC removes the need.
Tutorial
The Problem
Writing explicit lifetime annotations on every function would make Rust code extremely verbose. Lifetime elision rules were introduced to allow the compiler to infer annotations in the most common cases, making everyday code read cleanly. Three rules cover the vast majority of functions: (1) each input reference gets its own lifetime, (2) if there is exactly one input lifetime, it propagates to all output references, (3) if one of the inputs is &self or &mut self, its lifetime propagates to all output references. Understanding these rules explains when annotations are required and why.
🎯 Learning Outcomes
fn strlen(s: &str) -> usize expands to fn strlen<'a>(s: &'a str) -> usizefn first_word(s: &str) -> &str expands by Rule 2 (one input → output gets its lifetime)fn remaining(&self) -> &str on a struct expands by Rule 3 (&self lifetime)Code Example
// Elision Rule 1: Each input gets own lifetime
fn strlen(s: &str) -> usize // &'a str implicitly
// Elision Rule 2: One input → output gets same lifetime
fn first_word(s: &str) -> &str // &'a str → &'a str
// Elision Rule 3: &self → output gets self's lifetime
impl Parser { fn remaining(&self) -> &str }
// Multiple inputs: explicit required
fn longer<'a>(x: &'a str, y: &'a str) -> &'a strKey Differences
OCaml Approach
OCaml has no lifetime elision because there are no lifetime annotations to elide. All functions operate on GC-managed values, and no annotation is ever required:
let strlen s = String.length s
let first_word s = match String.split_on_char ' ' s with w :: _ -> w | [] -> ""
let longer x y = if String.length x >= String.length y then x else y
Full Source
#![allow(clippy::all)]
//! Lifetime Elision Rules
//!
//! When and how Rust infers lifetimes automatically.
/// Rule 1: Each input ref gets own lifetime.
/// Elided: fn strlen(s: &str) -> usize
/// Expanded: fn strlen<'a>(s: &'a str) -> usize
pub fn strlen(s: &str) -> usize {
s.len()
}
/// Rule 2: If one input lifetime, output gets it.
/// Elided: fn first_word(s: &str) -> &str
/// Expanded: fn first_word<'a>(s: &'a str) -> &'a str
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
/// Rule 3: If &self or &mut self, output gets self's lifetime.
pub struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
/// Elided: fn remaining(&self) -> &str
/// Expanded: fn remaining(&self) -> &'a str
pub fn remaining(&self) -> &str {
self.input
}
}
/// Multiple inputs: cannot elide output lifetime.
/// Must be explicit: fn longer<'a>(x: &'a str, y: &'a str) -> &'a str
pub fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() {
x
} else {
y
}
}
/// No elision needed for non-reference returns.
pub fn count_words(s: &str, _other: &str) -> usize {
s.split_whitespace().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strlen() {
assert_eq!(strlen("hello"), 5);
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
#[test]
fn test_parser_remaining() {
let parser = Parser {
input: "test input",
};
assert_eq!(parser.remaining(), "test input");
}
#[test]
fn test_longer() {
assert_eq!(longer("hi", "hello"), "hello");
}
#[test]
fn test_count_words() {
assert_eq!(count_words("a b c", "ignored"), 3);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strlen() {
assert_eq!(strlen("hello"), 5);
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
#[test]
fn test_parser_remaining() {
let parser = Parser {
input: "test input",
};
assert_eq!(parser.remaining(), "test input");
}
#[test]
fn test_longer() {
assert_eq!(longer("hi", "hello"), "hello");
}
#[test]
fn test_count_words() {
assert_eq!(count_words("a b c", "ignored"), 3);
}
}
Deep Comparison
OCaml vs Rust: Lifetime Elision
OCaml
(* No lifetime annotations ever needed *)
let strlen s = String.length s
let first_word s = List.hd (String.split_on_char ' ' s)
Rust
// Elision Rule 1: Each input gets own lifetime
fn strlen(s: &str) -> usize // &'a str implicitly
// Elision Rule 2: One input → output gets same lifetime
fn first_word(s: &str) -> &str // &'a str → &'a str
// Elision Rule 3: &self → output gets self's lifetime
impl Parser { fn remaining(&self) -> &str }
// Multiple inputs: explicit required
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str
Key Differences
Exercises
fn pick(cond: bool, a: &str, b: &str) -> &str — observe that elision fails here and add the correct annotation.fn peek(&self) -> char to Parser that returns the first character of self.input — verify elision applies Rule 3 correctly.