๐Ÿฆ€ 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

541: Lifetime Elision Rules

Difficulty: 3 Level: Intermediate 90% of Rust lifetimes are never written โ€” the compiler infers them using three deterministic rules. Knowing the rules tells you exactly when you must write `'a` and when the compiler will figure it out.

The Problem This Solves

Early Rust required every lifetime to be written explicitly. Every function signature involving references needed `<'a>` annotations. The community found that most lifetime annotations were obvious from the signature structure โ€” so Rust introduced elision rules to infer them automatically. Without knowing the rules, you can't tell whether a function signature is correct or just happens to compile. You also can't diagnose "missing lifetime specifier" errors โ€” you won't know which cases require annotation.

The Intuition

Lifetime elision is the compiler filling in lifetime annotations you didn't write. The three rules are applied in order to each function signature: If after applying all three rules, any output reference lifetime is still unknown, the compiler requires an explicit annotation.

How It Works in Rust

Rule 1 โ€” one input, output unrelated (no elision needed):
fn strlen(s: &str) -> usize { s.len() }
// Expands to: fn strlen<'a>(s: &'a str) -> usize
// No output reference โ€” rule 1 fires but rule 2 doesn't need to
Rule 2 โ€” single input reference, output gets that lifetime:
fn first_word(s: &str) -> &str {
 s.split_whitespace().next().unwrap_or("")
}
// Expands to: fn first_word<'a>(s: &'a str) -> &'a str
// Rule 1: s gets 'a. Rule 2: exactly one input lifetime โ†’ output gets 'a
Rule 3 โ€” `&self` method, output gets self's lifetime:
struct Cache { data: Vec<String> }

impl Cache {
 fn get(&self, index: usize) -> Option<&str> {
     self.data.get(index).map(|s| s.as_str())
 }
 // Expands to: fn get<'a>(&'a self, index: usize) -> Option<&'a str>
 // Rule 3: &self present โ†’ output gets 'a (self's lifetime)
}
When elision fails โ€” must annotate:
// Two input references, output reference, no self โ†’ rule 2 can't apply (two candidates)
fn longest(x: &str, y: &str) -> &str { ... }
// error: missing lifetime specifier
// Fix:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
Rule 2 works with one input even if there are more:
// Output comes from s, not prefix โ€” must annotate even though one "main" input
fn trim_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
 // 'a on s only; prefix gets its own lifetime but output is tied to s
 s.strip_prefix(prefix).unwrap_or(s)
}
The `'_` placeholder โ€” explicit elision:
// '_ means "infer the lifetime here" โ€” documents that a lifetime is present
fn get_first(v: &[i32]) -> Option<&'_ i32> { v.first() }
// Same as: fn get_first<'a>(v: &'a [i32]) -> Option<&'a i32>

What This Unlocks

Key Differences

ConceptOCamlRust
Reference annotationsNo reference annotations โ€” GC manages allLifetimes usually inferred via elision; explicit when ambiguous
Inference rulesFull Hindley-Milner type inference everywhereThree elision rules for lifetimes; everything else explicit
Method return referencesAlways valid (GC)Rule 3: `&self` methods return borrow tied to self by default
Single input functionsNo conceptRule 2: one input `&T` โ†’ output `&T` gets same lifetime
`'_` syntaxN/AExplicit elision marker โ€” "infer this lifetime" โ€” useful in type positions
//! # 541. Lifetime Elision Rules
//! When and how Rust infers lifetimes automatically.

// ============================================================
// RULE 1: Each input reference gets its own lifetime
// fn foo(x: &str) โ†’ fn foo<'a>(x: &'a str)
// fn bar(x: &str, y: &str) โ†’ fn bar<'a, 'b>(x: &'a str, y: &'b str)
// ============================================================

/// Elision rule 1: one input, inferred
fn strlen(s: &str) -> usize { // &str gets lifetime 'a implicitly
    s.len()
}

/// Multiple inputs each get own lifetime โ€” no output ref needed
fn print_both(a: &str, b: &str) {
    println!("{} and {}", a, b);
}

// ============================================================
// RULE 2: If exactly one input lifetime, it flows to output
// fn foo(x: &str) -> &str โ†’ fn foo<'a>(x: &'a str) -> &'a str
// ============================================================

/// Elision rule 2: one input &, output tied to it
fn first_word(s: &str) -> &str { // output gets same lifetime as s
    s.split_whitespace().next().unwrap_or("")
}

/// Trim with rule 2
fn trim_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
    // Two input refs โ€” must annotate: output from s, not prefix
    s.strip_prefix(prefix).unwrap_or(s)
}

// ============================================================
// RULE 3: If &self or &mut self, output tied to self's lifetime
// ============================================================

struct Cache {
    data: Vec<String>,
}

impl Cache {
    /// Rule 3: output tied to self โ€” no annotation needed
    fn get(&self, index: usize) -> Option<&str> { // &str has self's lifetime
        self.data.get(index).map(|s| s.as_str())
    }

    fn first(&self) -> Option<&str> { // same โ€” rule 3
        self.data.first().map(|s| s.as_str())
    }

    /// Returns owned String โ€” avoids lifetime conflict between self and default
    fn get_or(&self, index: usize, default: &str) -> String {
        // When output needs to come from different sources with different lifetimes,
        // returning an owned String is the clean solution
        self.data.get(index)
            .map(|s| s.clone())
            .unwrap_or_else(|| default.to_string())
    }
}

/// When elision DOESN'T apply โ€” must annotate
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
    // Two input refs, one output ref โ€” which does output borrow from?
    // Elision rule 2 only works with ONE input ref.
    // Rule 3 doesn't apply (no self). Must annotate.
    if x.len() >= y.len() { x } else { y }
}

fn main() {
    // Rule 1: no output ref
    println!("strlen: {}", strlen("hello world"));
    print_both("foo", "bar");

    // Rule 2: one input, output same lifetime
    let s = String::from("hello world foo");
    let word = first_word(&s);
    println!("first_word: {}", word);

    let prefixed = String::from("prefix_value");
    let trimmed = trim_prefix(&prefixed, "prefix_");
    println!("trimmed: {}", trimmed);

    // Rule 3: methods โ€” output tied to self
    let mut cache = Cache { data: vec!["apple".to_string(), "banana".to_string()] };
    println!("first: {:?}", cache.first());
    println!("get(1): {:?}", cache.get(1));

    // When you need explicit annotations
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("s2");
        result = longer(s1.as_str(), s2.as_str());
        println!("longer: {}", result);
    }
}

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

    #[test]
    fn test_first_word_elision() {
        let s = String::from("hello world");
        assert_eq!(first_word(&s), "hello");
    }

    #[test]
    fn test_trim_prefix() {
        let s = String::from("http://example.com");
        assert_eq!(trim_prefix(&s, "http://"), "example.com");
    }

    #[test]
    fn test_cache_rule3() {
        let c = Cache { data: vec!["x".to_string(), "y".to_string()] };
        assert_eq!(c.get(0), Some("x"));
        assert_eq!(c.first(), Some("x"));
        assert_eq!(c.get(99), None);
    }

    #[test]
    fn test_longer_explicit() {
        assert_eq!(longer("hello", "hi"), "hello");
        assert_eq!(longer("ab", "abc"), "abc");
    }
}
(* OCaml: no lifetime annotations needed ever *)
let first_word s =
  match String.split_on_char ' ' s with
  | [] -> ""
  | w :: _ -> w

let get_or_default opt default = match opt with
  | None -> default
  | Some v -> v

let () =
  Printf.printf "%s\n" (first_word "hello world");
  Printf.printf "%s\n" (get_or_default None "default");
  Printf.printf "%s\n" (get_or_default (Some "value") "default")