๐Ÿฆ€ Functional Rust
๐ŸŽฌ Rust Ownership in 30 seconds Visual walkthrough of ownership, moves, and automatic memory management.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Each value in Rust has exactly one owner โ€” when the owner goes out of scope, the value is dropped

โ€ข Assignment moves ownership by default; the original binding becomes invalid

โ€ข Borrowing (&T / &mut T) lets you reference data without taking ownership

โ€ข The compiler enforces: many shared references OR one mutable reference, never both

โ€ข No garbage collector needed โ€” memory is freed deterministically at scope exit

106: Lifetime Elision

Difficulty: 2 Level: Intermediate Three simple rules let the compiler infer lifetimes automatically in the most common cases โ€” so you only write annotations when the situation is ambiguous.

The Problem This Solves

If every function that takes or returns references needed explicit lifetime annotations, Rust would be extremely verbose. Early Rust (pre-1.0) actually required them everywhere. `fn first_word<'a>(s: &'a str) -> &'a str` for what should just be `fn first_word(s: &str) -> &str`. The annotations added noise without adding information โ€” the relationship was obvious. The lifetime elision rules were introduced to codify the patterns that appear in almost all real Rust code. They're not magic: the compiler applies three deterministic rules, and if those rules produce an unambiguous answer, the lifetime is inferred. If they don't, the compiler asks you to be explicit. This means you write annotations only when the relationship between input and output lifetimes is genuinely non-obvious. Understanding the rules also helps you read other people's Rust code: when you see `fn get_name(&self) -> &str`, you can mentally expand it to `fn get_name<'a>(&'a self) -> &'a str` and know exactly what it means.

The Intuition

The compiler has three simple rules for guessing lifetimes; when the rules give a clear answer, you write nothing โ€” only write annotations when you're doing something the rules can't figure out on their own.

How It Works in Rust

The three elision rules: 1. Each reference parameter gets its own lifetime. 2. If there's exactly one input lifetime, all output lifetimes get that lifetime. 3. If one of the inputs is `&self` or `&mut self`, all output lifetimes get `self`'s lifetime.
// Rule 2: one input โ†’ output gets its lifetime
fn first_word(s: &str) -> &str {
 // Expanded: fn first_word<'a>(s: &'a str) -> &'a str
 s.split_whitespace().next().unwrap_or("")
}

// Rule 3: &self method โ†’ output gets self's lifetime
struct Config {
 name: String,
}
impl Config {
 fn name(&self) -> &str {
     // Expanded: fn name<'a>(&'a self) -> &'a str
     &self.name
 }
}

// Rules don't resolve this โ€” must annotate explicitly
// Which input does the return value come from?
fn longest(s1: &str, s2: &str) -> &str {  // ERROR: ambiguous
 if s1.len() > s2.len() { s1 } else { s2 }
}

// Fixed: explicit annotation
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
 if s1.len() > s2.len() { s1 } else { s2 }
}

// Rule 1: multiple inputs, each gets its own lifetime (doesn't help resolve output)
fn first_of_two<'a>(s1: &'a str, _s2: &str) -> &'a str {
 // Explicitly says: return comes from s1, not s2
 s1
}

What This Unlocks

Key Differences

ConceptOCamlRust
Reference lifetimesNot tracked (GC handles)Inferred via elision rules or written explicitly
Common function signaturesNo annotations neededNo annotations needed (elided)
Ambiguous casesNot possible (GC)Compiler asks for explicit annotation
Learning curveNo lifetime conceptElision rules reduce annotation burden significantly
Documentation valueN/AExplicit `'a` annotations document reference relationships
// Example 106: Lifetime Elision Rules
//
// Rust has three elision rules that let you skip lifetime annotations:
// 1. Each input reference gets its own lifetime
// 2. If exactly one input lifetime, output gets that lifetime
// 3. If &self or &mut self, output gets self's lifetime

// Approach 1: Single input โ†’ elision applies (rule 2)
// fn first_word<'a>(s: &'a str) -> &'a str  โ† what compiler infers
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or(s)
}

fn approach1() {
    let word = first_word("hello world");
    assert_eq!(word, "hello");
    println!("First word: {}", word);
}

// Approach 2: Method with &self โ†’ elision applies (rule 3)
struct TextBuffer {
    content: String,
}

impl TextBuffer {
    // fn get_content<'a>(&'a self) -> &'a str  โ† inferred
    fn get_content(&self) -> &str {
        &self.content
    }
    
    fn get_length(&self) -> usize {
        self.content.len()
    }
}

fn approach2() {
    let buf = TextBuffer { content: "Hello, World!".into() };
    let c = buf.get_content();
    let l = buf.get_length();
    assert_eq!(l, 13);
    println!("Content: {}, Length: {}", c, l);
}

// Approach 3: Multiple inputs โ†’ MUST annotate (no elision)
fn pick_longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn approach3() {
    let result = pick_longer("hello", "hi");
    assert_eq!(result, "hello");
    println!("Longer: {}", result);
}

fn main() {
    println!("=== Approach 1: Single Input (Elided) ===");
    approach1();
    println!("\n=== Approach 2: Method &self (Elided) ===");
    approach2();
    println!("\n=== Approach 3: Multiple Inputs (Must Annotate) ===");
    approach3();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[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_text_buffer() {
        let buf = TextBuffer { content: "test".into() };
        assert_eq!(buf.get_content(), "test");
        assert_eq!(buf.get_length(), 4);
    }

    #[test]
    fn test_pick_longer() {
        assert_eq!(pick_longer("abc", "ab"), "abc");
        assert_eq!(pick_longer("x", "yy"), "yy");
    }

    #[test]
    fn test_elision_with_owned_param() {
        // No lifetime needed: input is owned, output is owned
        fn to_upper(s: &str) -> String {
            s.to_uppercase()
        }
        assert_eq!(to_upper("hi"), "HI");
    }
}
(* Example 106: Lifetime Elision โ€” When You Don't Need to Annotate *)

(* OCaml never needs lifetime annotations. This example shows the
   patterns where Rust also skips them thanks to elision rules. *)

(* Approach 1: Single input reference โ†’ output borrows from it *)
let first_word s =
  match String.index_opt s ' ' with
  | Some i -> String.sub s 0 i
  | None -> s

let approach1 () =
  let word = first_word "hello world" in
  assert (word = "hello");
  Printf.printf "First word: %s\n" word

(* Approach 2: Method-style โ€” self is the obvious source *)
type text_buffer = { content : string }

let get_content buf = buf.content
let get_length buf = String.length buf.content

let approach2 () =
  let buf = { content = "Hello, World!" } in
  let c = get_content buf in
  let l = get_length buf in
  assert (l = 13);
  Printf.printf "Content: %s, Length: %d\n" c l

(* Approach 3: Multiple inputs โ€” OCaml doesn't care *)
let pick_longer a b =
  if String.length a >= String.length b then a else b

let approach3 () =
  let result = pick_longer "hello" "hi" in
  assert (result = "hello");
  Printf.printf "Longer: %s\n" result

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Lifetime Elision

Single Input Reference

OCaml:

๐Ÿช Show OCaml equivalent
let first_word s =
match String.index_opt s ' ' with
| Some i -> String.sub s 0 i
| None -> s

Rust โ€” lifetime elided:

fn first_word(s: &str) -> &str {  // compiler infers: both are 'a
 s.split_whitespace().next().unwrap_or(s)
}

Methods on &self

OCaml:

๐Ÿช Show OCaml equivalent
let get_content buf = buf.content

Rust โ€” &self lifetime elided:

impl TextBuffer {
 fn get_content(&self) -> &str {  // output borrows from self
     &self.content
 }
}

When Elision Doesn't Work

OCaml โ€” never an issue:

๐Ÿช Show OCaml equivalent
let pick_longer a b =
if String.length a >= String.length b then a else b

Rust โ€” must annotate with multiple input lifetimes:

fn pick_longer<'a>(a: &'a str, b: &'a str) -> &'a str {
 if a.len() >= b.len() { a } else { b }
}