| name | matsakis-ownership-mastery |
| description | 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. |
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
fn print(s: &str) { }
fn first_word(s: &str) -> &str { }
impl MyStruct {
fn method(&self) -> &str { }
}
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>,
}
fn bad(data: &mut Data) {
let f1 = &mut data.field1;
let f2 = &mut data.field2;
}
fn good(data: &mut Data) {
let Data { field1, field2 } = data;
field1.push_str("hello");
field2.push(42);
}
impl Data {
fn split(&mut self) -> (&mut String, &mut Vec<i32>) {
(&mut self.field1, &mut self.field2)
}
}
The Borrow Checker as Design Guide
fn bad_design(items: &mut Vec<String>) {
for item in items.iter() {
if item.starts_with("remove") {
items.retain(|s| s != item);
}
}
}
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));
}
fn better_design(items: &mut Vec<String>) {
items.retain(|s| !s.starts_with("remove"));
}
Lifetime Bounds in Generics
struct Parser<'input> {
input: &'input str,
}
impl<'input> Parser<'input> {
fn parse(&self) -> Token<'input> {
Token { text: &self.input[0..5] }
}
}
fn process<'a, T>(item: &'a T) -> &'a str
where
T: AsRef<str> + 'a,
{
item.as_ref()
}
fn with_callback<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
let s = String::from("hello");
let result = (&s);
(, result);
}
Interior Mutability (When Needed)
use std::cell::{Cell, RefCell};
struct Counter {
count: Cell<usize>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
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
}
}
Self-Referential Structs (The Right Way)
struct Good {
data: String,
start: usize,
end: usize,
}
impl Good {
fn slice(&self) -> &str {
&self.data[self.start..self.end]
}
}
use std::pin::Pin;
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?