the borrow checker, explained to my ruby self
three errors every beginner hits, what they actually mean, and how i stopped fighting them
Contents
i wrote ruby for five years before rust. in ruby every variable is a reference to an object somewhere, the garbage collector cleans up after you, and you never think about who "owns" anything. so my first month of rust was mostly me yelling at the compiler. here are the three errors i hit most, translated for past me.
if you want the 100 second version of rust first, fireship's is honestly good:
1. E0382: the value moved
let names = vec![String::from("mio"), String::from("emeka")];
let moved = names;
println!("{:?}", names);
// error[E0382]: borrow of moved value: `names`
in ruby, moved = names gives you two names for the same array. in rust, it moves ownership. a Vec owns heap memory, and rust wants exactly one owner so it knows exactly when to free it. after the move, names is not allowed anymore.
fixes: borrow it instead (let view = &names;) if you only need to read, or names.clone() if you really want two copies. clone is fine while learning. really.
2. E0502: reading and writing at the same time
let mut queue = vec![1, 2, 3];
let first = &queue[0];
queue.push(4);
println!("{first}");
// error[E0502]: cannot borrow `queue` as mutable because it is also borrowed as immutable
this one made me angry until i understood it. push may need more space, so the vec can move all its elements to a new place in memory. first would then point at the old place. in c that is a dangling pointer and a bad week. rust refuses to compile it.
the rule in one line: one writer OR many readers, never both at the same time. fixes: copy the value (let first = queue[0];, an i32 is Copy), or finish using first before you push. the compiler is smart about this, a borrow ends after its last use, not at the end of the block.
3. E0515: returning a reference to something that dies
fn longest_name(names: &[String]) -> &str {
let mut best = String::new();
for n in names {
if n.len() > best.len() {
best = n.clone();
}
}
&best
// error[E0515]: cannot return reference to local variable `best`
}
best lives inside the function. when the function returns, it is freed. a reference to it would point at nothing. the fix is to return a reference into the data the caller gave you, which lives long enough:
fn longest_name(names: &[String]) -> Option<&str> {
names.iter().map(|n| n.as_str()).max_by_key(|n| n.len())
}
no lifetime annotations needed, rust figures out that the result borrows from names. and Option because the slice can be empty, which my ruby version silently returned "" for.
what helped me stop fighting
- read the whole error. rust errors are long because they are useful.
rustc --explain E0502gives a full explanation with examples. - own your data first. use
Stringin structs, not&str, until you have a reason. lifetimes in structs can wait. - clone when stuck, optimise later. a clone you can see is better than a bug you can't.
and because danny will ask: our old ruby service did p99 180 ms and 1.2 GB of memory. first we fixed an n+1 in the ruby code and it went to 60 ms. then the rust version: p99 11 ms, 40 MB. the n+1 was the bigger win, the rust part is real too 🦀❄️
Written by
Mio Kobayashi
rust backend @ a small logistics startup in sapporo. ex-ruby. they/them. snow, ramen, borrow checker 🦀❄️
Get the best of #types-go-brr weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to comment