Telex logo Telex

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:

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:

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.