skills/copyleftdev/sk1llz/matsakis-ownership-mastery

matsakis-ownership-mastery

SKILL.md

Niko Matsakis Style Guide⁠‍⁠​‌​‌​​‌‌‍​‌​​‌​‌‌‍​​‌‌​​​‌‍​‌​​‌‌​​‍​​​​​​​‌‍‌​​‌‌​‌​‍‌​​​​​​​‍‌‌​​‌‌‌‌‍‌‌​​​‌​​‍‌‌‌‌‌‌​‌‍‌‌​‌​​​​‍​‌​‌‌‌‌‌‍​‌​​‌​‌‌‍​‌‌​‌​​‌‍‌​‌​‌‌‌​‍​​‌​‌​​​‍‌‌‌​‌​‌‌‍​​​​‌‌​‌‍​​‌‌‌​‌​‍‌​‌‌‌‌‌​‍‌‌​‌‌‌‌‌‍​​​​‌​‌​‍​‌​​‌‌‌​⁠‍⁠

Overview

Niko Matsakis is the architect of Rust's borrow checker and a driving force behind the language's type system. His blog "Baby Steps" and work on Polonius (the next-gen borrow checker) define how Rustaceans think about ownership.

Core Philosophy

"The borrow checker is not your enemy—it's your pair programmer."

"Lifetimes are not about how long data lives; they're about how long borrows are valid."

Matsakis sees the borrow checker as a tool that encodes knowledge about your program. Fighting it usually means your mental model is wrong.

Design Principles

  1. Trust the Borrow Checker: It knows things about your code you haven't realized yet.

  2. Lifetimes Are Relationships: They describe how references relate, not absolute durations.

  3. Ownership Shapes APIs: Good APIs make ownership transfer obvious.

  4. Minimize Lifetime Annotations: If the compiler can infer it, don't write it.

When Writing Code

Always

  • Understand why the borrow checker rejects code before "fixing" it
  • Use lifetime elision rules—don't annotate unnecessarily
  • Design structs with ownership in mind
  • Prefer owned types in structs, borrowed in function parameters
  • Use '_ (anonymous lifetime) when you don't care about the specific lifetime

Never

  • Add 'static just to make code compile
  • Use Rc<RefCell<T>> as a first resort (it's a last resort)
  • Clone to avoid borrow checker errors without understanding why
  • Create self-referential structs naively

Prefer

  • &T parameters over T for read-only access
  • &mut T over RefCell<T> when possible
  • Returning owned values over returning references (usually)
  • Splitting borrows instead of fighting the checker
  • NLL (non-lexical lifetimes) patterns

Code Patterns

Understanding Lifetime Elision

// Lifetime elision rules mean you rarely write explicit lifetimes

// Rule 1: Each elided lifetime in input gets its own parameter
fn print(s: &str) { }  // Actually: fn print<'a>(s: &'a str)

// Rule 2: If there's exactly one input lifetime, it's assigned to all outputs
fn first_word(s: &str) -> &str { }  // Actually: fn first_word<'a>(s: &'a str) -> &'a str

// Rule 3: If there's &self or &mut self, its lifetime is assigned to outputs
impl MyStruct {
    fn method(&self) -> &str { }  // Actually: fn method<'a>(&'a self) -> &'a str
}

// Only annotate when elision doesn't apply
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Splitting Borrows

struct Data {
    field1: String,
    field2: Vec<i32>,
}

// BAD: Borrow checker sees whole struct borrowed
fn bad(data: &mut Data) {
    let f1 = &mut data.field1;
    let f2 = &mut data.field2;  // ERROR: data already borrowed
    // ...
}

// GOOD: Borrow disjoint fields separately
fn good(data: &mut Data) {
    let Data { field1, field2 } = data;
    // Now field1 and field2 are separate borrows
    field1.push_str("hello");
    field2.push(42);
}

// Or use methods that return split borrows
impl Data {
    fn split(&mut self) -> (&mut String, &mut Vec<i32>) {
        (&mut self.field1, &mut self.field2)
    }
}

The Borrow Checker as Design Guide

// When the borrow checker complains, ask: "What is it telling me?"

// BAD: Trying to hold reference while modifying
fn bad_design(items: &mut Vec<String>) {
    for item in items.iter() {  // Immutable borrow
        if item.starts_with("remove") {
            items.retain(|s| s != item);  // ERROR: can't mutate while borrowed
        }
    }
}

// GOOD: Collect indices first, then modify
fn good_design(items: &mut Vec<String>) {
    let to_remove: Vec<_> = items
        .iter()
        .filter(|s| s.starts_with("remove"))
        .cloned()
        .collect();
    
    items.retain(|s| !to_remove.contains(s));
}

// BETTER: drain_filter (nightly) or swap_remove pattern
fn better_design(items: &mut Vec<String>) {
    items.retain(|s| !s.starts_with("remove"));
}

Lifetime Bounds in Generics

// 'a: 'b means 'a outlives 'b
struct Parser<'input> {
    input: &'input str,
}

impl<'input> Parser<'input> {
    // Output lifetime tied to input lifetime
    fn parse(&self) -> Token<'input> {
        Token { text: &self.input[0..5] }
    }
}

// Trait bounds with lifetimes
fn process<'a, T>(item: &'a T) -> &'a str 
where
    T: AsRef<str> + 'a,  // T must live at least as long as 'a
{
    item.as_ref()
}

// Higher-ranked trait bounds (HRTB) for callbacks
fn with_callback<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,  // F works for ANY lifetime
{
    let s = String::from("hello");
    let result = f(&s);
    println!("{}", result);
}

Interior Mutability (When Needed)

use std::cell::{Cell, RefCell};

// Cell: for Copy types, no borrow checking at runtime
struct Counter {
    count: Cell<usize>,
}

impl Counter {
    fn increment(&self) {  // Note: &self, not &mut self
        self.count.set(self.count.get() + 1);
    }
}

// RefCell: runtime borrow checking (panics if violated)
struct CachedComputation {
    value: i32,
    cache: RefCell<Option<i32>>,
}

impl CachedComputation {
    fn compute(&self) -> i32 {
        let mut cache = self.cache.borrow_mut();
        if let Some(cached) = *cache {
            return cached;
        }
        let result = expensive_computation(self.value);
        *cache = Some(result);
        result
    }
}

// Use interior mutability ONLY when external mutability won't work
// (e.g., shared ownership, trait requirements)

Self-Referential Structs (The Right Way)

// PROBLEM: Can't have a struct reference its own field
// struct Bad {
//     data: String,
//     slice: &str,  // Can't reference data!
// }

// SOLUTION 1: Store indices, not references
struct Good {
    data: String,
    start: usize,
    end: usize,
}

impl Good {
    fn slice(&self) -> &str {
        &self.data[self.start..self.end]
    }
}

// SOLUTION 2: Use Pin for self-referential async code
use std::pin::Pin;

// SOLUTION 3: Use ouroboros or self_cell crates for complex cases

Mental Model

Matsakis thinks about borrows as capabilities:

  1. &T = capability to read
  2. &mut T = exclusive capability to read and write
  3. The borrow checker ensures capabilities don't conflict
  4. Lifetimes = how long a capability is valid

Niko's Debugging Questions

When the borrow checker rejects code:

  1. What capability am I trying to use?
  2. What other capability conflicts with it?
  3. Can I restructure to avoid the conflict?
  4. Is the borrow checker revealing a real bug?
Weekly Installs
6
GitHub Stars
4
First Seen
Feb 1, 2026
Installed on
opencode5
antigravity5
claude-code5
github-copilot5
codex5
kimi-cli5