Lifetime Annotations: 'a Basics
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lifetime Annotations: 'a Basics" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Memory safety without a garbage collector requires the compiler to track how long references are valid. Key difference from OCaml: 1. **Compile
Tutorial
The Problem
Memory safety without a garbage collector requires the compiler to track how long references are valid. C and C++ leave this to the programmer, leading to use-after-free bugs — one of the most common sources of security vulnerabilities (CVEs). Rust solves this with lifetimes: explicit annotations that encode reference validity constraints in the type system. When a function returns a reference, the compiler needs to know which input the output borrows from, so it can reject code that would create a dangling pointer. Lifetime annotations are the mechanism for expressing this relationship.
🎯 Learning Outcomes
'a in fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str constrains the outputstruct Excerpt<'a> { part: &'a str }Code Example
// Explicit lifetime: output valid while both inputs valid
pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}
// Elided lifetime: compiler infers input → output
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}Key Differences
struct Excerpt<'a> must annotate the lifetime of borrowed fields; OCaml records holding references need no annotation — the GC manages all values.OCaml Approach
OCaml has no lifetime annotations — the garbage collector ensures referenced values remain alive as long as any reference exists. The equivalent of longer is:
let longer s1 s2 = if String.length s1 >= String.length s2 then s1 else s2
There is no annotation needed, and the GC prevents dangling references. However, this means OCaml programs cannot express "this reference is borrowed, not owned" at the type level.
Full Source
#![allow(clippy::all)]
//! Lifetime Annotations: 'a Basics
//!
//! Explicit lifetime parameters expressing reference validity constraints.
/// Return the longer of two string slices.
/// 'a means: output valid as long as BOTH inputs are valid.
pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
/// First word of a string: output tied to input's lifetime.
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
/// Two different lifetimes: x and y may have different scopes.
pub fn pick_first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x // output tied to 'a only
}
/// Struct holding a reference — must annotate lifetime.
#[derive(Debug)]
pub struct Excerpt<'a> {
pub part: &'a str,
}
impl<'a> Excerpt<'a> {
pub fn new(text: &'a str) -> Self {
Excerpt { part: text }
}
pub fn part(&self) -> &str {
self.part
}
}
/// Function returning a reference to one of its inputs.
pub fn min_by_len<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() {
a
} else {
b
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longer() {
let s1 = "hello";
let s2 = "hi";
assert_eq!(longer(s1, s2), "hello");
}
#[test]
fn test_longer_equal() {
let s1 = "abc";
let s2 = "xyz";
assert_eq!(longer(s1, s2), "abc"); // first wins on tie
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
assert_eq!(first_word(""), "");
}
#[test]
fn test_pick_first() {
let x = "first";
let y = "second";
assert_eq!(pick_first(x, y), "first");
}
#[test]
fn test_excerpt() {
let text = String::from("Call me Ishmael.");
let excerpt = Excerpt::new(&text[..7]);
assert_eq!(excerpt.part(), "Call me");
}
#[test]
fn test_min_by_len() {
assert_eq!(min_by_len("hi", "hello"), "hi");
assert_eq!(min_by_len("abc", "ab"), "ab");
}
#[test]
fn test_lifetime_scope() {
let result;
{
let s1 = String::from("long string");
let s2 = String::from("short");
result = longer(&s1, &s2);
assert_eq!(result, "long string");
}
// result is no longer valid here because s1, s2 are dropped
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longer() {
let s1 = "hello";
let s2 = "hi";
assert_eq!(longer(s1, s2), "hello");
}
#[test]
fn test_longer_equal() {
let s1 = "abc";
let s2 = "xyz";
assert_eq!(longer(s1, s2), "abc"); // first wins on tie
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
assert_eq!(first_word(""), "");
}
#[test]
fn test_pick_first() {
let x = "first";
let y = "second";
assert_eq!(pick_first(x, y), "first");
}
#[test]
fn test_excerpt() {
let text = String::from("Call me Ishmael.");
let excerpt = Excerpt::new(&text[..7]);
assert_eq!(excerpt.part(), "Call me");
}
#[test]
fn test_min_by_len() {
assert_eq!(min_by_len("hi", "hello"), "hi");
assert_eq!(min_by_len("abc", "ab"), "ab");
}
#[test]
fn test_lifetime_scope() {
let result;
{
let s1 = String::from("long string");
let s2 = String::from("short");
result = longer(&s1, &s2);
assert_eq!(result, "long string");
}
// result is no longer valid here because s1, s2 are dropped
}
}
Deep Comparison
OCaml vs Rust: Lifetime Basics
OCaml
(* No lifetimes needed — GC manages memory *)
let longer s1 s2 =
if String.length s1 >= String.length s2 then s1 else s2
let first_word s =
match String.split_on_char ' ' s with
| [] -> ""
| w :: _ -> w
Rust
// Explicit lifetime: output valid while both inputs valid
pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}
// Elided lifetime: compiler infers input → output
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Key Differences
Exercises
fn longest_word<'a>(sentence: &'a str) -> &'a str that returns a slice pointing into the original string for the longest whitespace-separated word.struct Pair<'a, 'b> { first: &'a str, second: &'b str } and a method fn shorter(&self) -> &str — figure out what lifetime the return type needs.fn inner_word<'a>(outer: &'a str, _separator: &str) -> &'a str that returns the substring before the first comma, tied only to outer's lifetime.