matsakis-ownership-mastery

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "matsakis-ownership-mastery" with this command: npx skills add copyleftdev/sk1llz/copyleftdev-sk1llz-matsakis-ownership-mastery

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

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

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

Ownership Shapes APIs: Good APIs make ownership transfer obvious.

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:

  • &T = capability to read

  • &mut T = exclusive capability to read and write

  • The borrow checker ensures capabilities don't conflict

  • Lifetimes = how long a capability is valid

Niko's Debugging Questions

When the borrow checker rejects code:

  • What capability am I trying to use?

  • What other capability conflicts with it?

  • Can I restructure to avoid the conflict?

  • Is the borrow checker revealing a real bug?

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

renaissance-statistical-arbitrage

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

google-material-design

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

aqr-factor-investing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

minervini-swing-trading

No summary provided by upstream source.

Repository SourceNeeds Review