Rust's _or Methods (and Five Other Naming Patterns) in Plain English
You open the standard library docs and see unwrap_or,
unwrap_or_else, unwrap_or_default,
as_ref, to_owned, into_inner,
filter_map, take_while, min_by_key,
copied, cloned. It looks like a mountain of
unrelated methods. It isn't. Just six naming conventions cover roughly
80% of the standard library. Once they click, methods you've never seen before become guessable.
1. _or vs _or_else -- eager value vs lazy closure
This is the most pervasive. Whenever you see a pair like this, the
_or version takes a value you already have. The
_or_else version takes a closure that computes the value on demand.
Same pattern in unwrap_or / unwrap_or_else,
ok_or / ok_or_else,
or / or_else.
Once you see it once, you see it everywhere.
let a: Option<i32> = None;
// _or: you already have the fallback value
let x = a.unwrap_or(42);
// _or_else: compute the fallback only if needed
let y = a.unwrap_or_else(|| expensive_default());
The same pairs appear across Option and Result:
// Option
unwrap_or(val) / unwrap_or_else(|| ...)
or(other_option) / or_else(|| ...)
// Result
unwrap_or(val) / unwrap_or_else(|err| ...)
ok_or(err) / ok_or_else(|| ...)
or(other_result) / or_else(|err| ...)
Why bother with the closure version? Because the value in _or
is always evaluated, even when it isn't needed. If creating the fallback is
expensive -- a heap allocation, a database lookup, a computation --
_or_else avoids the work when the Option is
Some or the Result is Ok.
// This allocates a String every time, even when config has a value:
let name = config.unwrap_or(String::from("default"));
// This only allocates when config is None:
let name = config.unwrap_or_else(|| String::from("default"));
2. as_ vs to_ vs into_ -- how much work is being done
This tells you the cost and the ownership story at a glance:
as_-- a cheap borrow. Just a different view of the same thing. No allocation, no cloning. Think: peeking.to_-- creates something new but leaves the original alone. Usually involves allocation or conversion. Think: photocopying.into_-- consumes the original and transforms it. The original is gone. Think: metamorphosing.
let s = String::from("hello");
// as_ -- borrows, cheap, returns a reference
let bytes: &[u8] = s.as_bytes(); // &String -> &[u8], no copy
let slice: &str = s.as_str(); // &String -> &str, no copy
// to_ -- copies/converts, original untouched
let upper: String = s.to_uppercase(); // creates a new String
let owned: String = "hi".to_owned(); // &str -> String, allocates
// into_ -- consumes, original is gone
let bytes: Vec<u8> = s.into_bytes(); // String consumed, Vec reuses its buffer
// println!("{s}"); // ERROR: s has been moved
as_ is O(1) and virtually free. to_ usually
allocates. into_ is often more efficient than
to_ because it can reuse the existing buffer instead of
copying it.
The pattern repeats across the standard library:
Path::as_os_str() // borrow: &Path -> &OsStr
Path::to_path_buf() // copy: &Path -> PathBuf
PathBuf::into_os_string() // move: PathBuf -> OsString
OsStr::as_encoded_bytes() // borrow
OsStr::to_os_string() // copy
OsString::into_string() // move
3. _map as a combiner -- two operations in one pass
When a method ends in _map, it performs a transformation
as part of another operation. The second word is always
map because mapping is the universal glue operation:
let v = vec![1, 2, 3, 4, 5];
// filter_map = filter + map in one step
// Return Some(value) to keep, None to discard
let evens_doubled: Vec<i32> = v.iter()
.filter_map(|&x| if x % 2 == 0 { Some(x * 2) } else { None })
.collect();
// [4, 8]
// find_map = find + map -- find the first match and transform it
let first_even_squared: Option<i32> = v.iter()
.find_map(|&x| if x % 2 == 0 { Some(x * x) } else { None });
// Some(4)
// flat_map = map + flatten -- map each element to an iterator, then flatten
let words = vec!["hello world", "foo bar"];
let all: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
// ["hello", "world", "foo", "bar"]
The pattern works on Option and Result too,
not just iterators:
let maybe_str: Option<&str> = Some("42");
// and_then is Option's version of flat_map:
// map would give Option<Result>, and_then flattens to Option
let parsed: Option<i32> = maybe_str.and_then(|s| s.parse().ok());
4. _while adds a predicate
The base method takes a count. The _while variant takes a
condition instead. take takes a count,
take_while takes a condition. Same for skip
vs skip_while:
let v = vec![1, 2, 3, 4, 5];
// take(n) -- take exactly n elements
let first_three: Vec<&i32> = v.iter().take(3).collect();
// [1, 2, 3]
// take_while(predicate) -- take while condition holds
let small: Vec<&i32> = v.iter().take_while(|&&x| x < 4).collect();
// [1, 2, 3]
// skip(n) -- skip exactly n elements
let after_two: Vec<&i32> = v.iter().skip(2).collect();
// [3, 4, 5]
// skip_while(predicate) -- skip while condition holds
let from_three: Vec<&i32> = v.iter().skip_while(|&&x| x < 3).collect();
// [3, 4, 5]
The pattern is consistent: the base version works with a fixed count,
_while makes it dynamic. Crucially, these are
short-circuiting: they stop as soon as the condition returns
false and will not process subsequent elements.
5. _by / _by_key -- "by what measure?"
The base method uses natural ordering (Ord). The
_by variant lets you supply a custom comparator. The
_by_key variant lets you say "compare by this extracted
value":
let words = vec!["cherry", "fig", "apple", "date"];
// min() -- natural ordering (alphabetical for strings)
let first = words.iter().min();
// Some("apple")
// min_by -- custom comparator
let shortest = words.iter()
.min_by(|a, b| a.len().cmp(&b.len()));
// Some("fig")
// min_by_key -- compare by extracted key (simpler when you can)
let shortest = words.iter()
.min_by_key(|w| w.len());
// Some("fig")
The same trio appears for max, sort, and
others:
// Sorting
v.sort(); // natural order
v.sort_by(|a, b| b.cmp(a)); // custom comparator (here: reverse)
v.sort_by_key(|item| item.score); // compare by extracted field
// Max
v.iter().max(); // natural order
v.iter().max_by(|a, b| ...); // custom comparator
v.iter().max_by_key(|x| ...); // compare by extracted key
Use _by_key for comparing a single property or derived
value. Use _by for complex logic, such as multi-field
tie-breaking or custom sort orders.
6. copied vs cloned -- how expensive is the copy?
Both do the same thing: turn an iterator of references into an iterator
of owned values. Same idea (get an owned value from a reference), but
copied is for small cheap types and cloned is
for anything:
let v = vec![1, 2, 3];
// copied -- requires T: Copy (small, cheap types: integers, bools, floats)
let owned: Vec<i32> = v.iter().copied().collect();
// cloned -- requires T: Clone (anything, but might be expensive)
let owned: Vec<i32> = v.iter().cloned().collect();
Both work here because integers implement both Copy and
Clone. The difference matters for intent and guarantees:
let strings = vec![String::from("a"), String::from("b")];
// strings.iter().copied() -- WON'T COMPILE: String isn't Copy
let owned: Vec<String> = strings.iter().cloned().collect(); // works
If it compiles with copied, prefer it. It signals to the
reader that the copy is trivial -- a memcpy of a few bytes, not a heap
allocation. When you see cloned, you know something more
expensive might be happening.
Putting it all together
These six conventions are composable. You can predict method names you've never seen:
ok_or_else-- convertsOptiontoResult, computing the error lazily (_or_else= closure)into_keys-- consumes aHashMapand gives you owned keys (into_= consuming)max_by_key-- finds the maximum by an extracted value (_by_key= extracted comparison)to_ascii_uppercase-- creates a new value, leaves the original (to_= new allocation)
When you encounter a new type in the standard library, you can often guess
what methods it has. Does it have as_ref? Probably. Does it
have into_inner? If it wraps something, yes. Does the
iterator adapter have a _by_key variant? Check, but likely.
The naming isn't arbitrary. It's a system. Learn these six meta-patterns and the standard library stops feeling like a phone book and starts feeling like a language you can read.
Telex