Claude-skill-registry-data matsakis-ownership-mastery
Write Rust code in the style of Niko Matsakis, Rust language team lead. Emphasizes deep understanding of ownership, lifetimes, and the borrow checker. Use when working with complex lifetime scenarios or designing APIs that interact with the ownership system.
git clone https://github.com/majiayu000/claude-skill-registry-data
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/matsakis" ~/.claude/skills/majiayu000-claude-skill-registry-data-matsakis-ownership-mastery && rm -rf "$T"
data/matsakis/SKILL.mdNiko 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
just to make code compile'static - Use
as a first resort (it's a last resort)Rc<RefCell<T>> - Clone to avoid borrow checker errors without understanding why
- Create self-referential structs naively
Prefer
parameters over&T
for read-only accessT
over&mut T
when possibleRefCell<T>- 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:
= capability to read&T
= exclusive capability to read and write&mut T- 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?