Rust Standard Library — API Selection and Composition
Authority: Rust Standard Library — the canonical reference. Read references/std-module-index.md for the full module map.
This skill owns choosing and composing std APIs: which collection, which smart pointer, which string type, which I/O trait, which iterator pattern. It does not own language semantics (rust-stable), async runtime (rust-concurrency), or application-layer concerns (the domain skills).
Capability Boundaries
✅ Strengths
- Picking the right collection (HashMap vs BTreeMap vs Vec vs VecDeque vs LinkedList vs BinaryHeap)
- Choosing the right smart pointer (Box vs Rc vs Arc vs RefCell vs Mutex vs OnceLock)
- Selecting the right string type (String vs &str vs OsString vs PathBuf vs Cow)
- Choosing interior mutability primitive (Cell vs RefCell vs OnceCell vs OnceLock vs LazyLock)
- Designing Option/Result combinator chains (map, andthen, unwraporelse, okor, transpose)
- Selecting I/O trait (Read vs Write vs BufRead vs Seek vs AsyncRead)
- Choosing iterator pattern (iter vs into_iter vs owning iterator, lazy vs collect)
- Using std concurrency primitives (thread, mpsc, barrier, Once, scoped threads)
- Selecting time API (Duration vs Instant vs SystemTime vs TryFromFloat)
- Cross-platform path handling (Path vs PathBuf, fs, env, process::Command)
⚠️ Prerequisites
- Rust ownership and lifetimes — see
rust-stable
- Cargo and project structure — see
rust-cargo-build, rust-workspace
❌ Out of Scope
- Async runtime (Tokio, async-std) →
rust-concurrency
- Language semantics (ownership, traits, lifetimes) →
rust-stable
- Application-layer concerns (CLI, web, database) → domain skills
- Unsafe and FFI →
rust-unsafe-ffi
Data Privacy
This skill does not collect, store, or transmit user data.
Part 1: Collections
Decision tree
Need a collection?
│
├─ Key-value lookup?
│ ├─ Order doesn't matter, fast lookup → HashMap<K, V>
│ ├─ Need sorted iteration / range queries → BTreeMap<K, V>
│ └─ Tiny (<20 entries), linear scan OK → Vec<(K, V)> with .iter().find()
│
├─ Sequence?
│ ├─ Append/pop from end → Vec<T>
│ ├─ Push/pop from both ends → VecDeque<T>
│ ├─ Insert/remove from middle frequently → LinkedList<T> (rare; usually Vec is better)
│ └─ Priority queue / always-min-first → BinaryHeap<T>
│
├─ Set?
│ ├─ Fast membership, no order → HashSet<T>
│ └─ Sorted iteration → BTreeSet<T>
│
└─ No collection needed — use Vec<T>
Quick reference
| Type |
Order |
Lookup |
Insert |
When |
Vec<T> |
insertion |
O(n) by value, O(1) by index |
O(1) amortized push |
Default sequence |
VecDeque<T> |
insertion |
O(1) front/back |
O(1) front/back |
Queue, sliding window |
LinkedList<T> |
insertion |
O(n) |
O(1) anywhere with cursor |
Almost never; prefer Vec |
BinaryHeap<T> |
priority |
O(1) peek |
O(log n) push/pop |
Priority queue |
HashMap<K,V> |
none |
O(1) avg |
O(1) avg |
Fast lookup, no order |
BTreeMap<K,V> |
sorted |
O(log n) |
O(log n) |
Ordered keys, range queries |
HashSet<T> |
none |
O(1) avg |
O(1) avg |
Membership |
BTreeSet<T> |
sorted |
O(log n) |
O(log n) |
Sorted membership |
See references/collections.md for allocation behavior, hasher selection (BuildHasherDefault/FxHashMap), and entry API patterns.
Entry API — avoid double lookup
use std::collections::HashMap;
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1; // single hash, no double lookup
}
Part 2: Smart Pointers
Decision tree
Need shared/indirected ownership?
│
├─ Single owner, heap-allocated → Box<T>
│
├─ Multiple readers, single thread → Rc<T>
│ └─ Need to mutate through shared ref → Rc<RefCell<T>>
│
├─ Multiple readers, multiple threads → Arc<T>
│ └─ Need to mutate through shared ref → Arc<Mutex<T>> or Arc<RwLock<T>>
│
├─ Need lazy one-time init → OnceLock<T> (sync) or once_cell::Lazy
│
└─ Need interior mutability (no shared ownership)?
├─ Copy type, simple → Cell<T>
├─ Non-Copy, complex → RefCell<T> (single thread)
└─ Multi-thread → Mutex<T> or RwLock<T>
Quick reference
| Type |
Ownership |
Thread-safe |
Mutability |
Use |
Box<T> |
Single |
Yes |
Direct (owned) |
Heap allocation, recursive types |
Rc<T> |
Shared |
No |
Immutable only |
Reference counting, single-threaded |
Arc<T> |
Shared |
Yes |
Immutable only |
Atomic RC, multi-threaded |
Cell<T> |
Single |
No |
Interior (Copy only) |
Simple mutable state |
RefCell<T> |
Single |
No |
Interior (borrow check at runtime) |
Mutation behind shared ref |
Mutex<T> |
Single |
Yes |
Interior (lock) |
Shared mutable state, sync |
RwLock<T> |
Single |
Yes |
Interior (read/write lock) |
Many readers, few writers |
OnceLock<T> |
Single |
Yes |
One-time init |
Lazy globals, std::sync::OnceLock |
LazyLock<T> |
Single |
Yes |
One-time init, transparent deref |
Const-ish globals (Rust 1.80+) |
See references/smart-pointers.md for trade-offs, drop ordering, and Weak<T> for cycles.
Part 3: String Types
Decision tree
Working with text?
│
├─ Function parameter → &str (always)
├─ Owned, mutable, UTF-8 → String
├─ Borrowed, UTF-8, with mixed owned/borrowed → Cow<'a, str>
├─ OS path (Windows/Unix native) → OsString (owned) / OsStr (borrowed)
├─ Filesystem path → PathBuf (owned) / Path (borrowed)
└─ Bytes that might not be UTF-8 → Vec<u8> / &[u8]
Quick reference
| Type |
Encoding |
Owned? |
Use |
&str |
UTF-8 |
No |
Function params, string literals |
String |
UTF-8 |
Yes |
Owned mutable UTF-8 text |
OsString/OsStr |
Platform-native |
Yes/No |
Environment, process args |
PathBuf/Path |
Platform-native |
Yes/No |
Filesystem paths |
Cow<'a, str> |
UTF-8 |
Maybe |
Zero-copy when possible, owned when needed |
Vec<u8>/&[u8] |
Bytes |
Yes/No |
Binary data, non-UTF-8 |
See references/string-types.md for conversion (.tostring() vs .toowned() vs .into()), Cow usage, and &str slicing safety (ischarboundary).
Part 4: Interior Mutability
Cell vs RefCell vs OnceCell
use std::cell::{Cell, RefCell, OnceCell};
// Cell: only for Copy types, no borrow checking
let counter = Cell::new(0);
counter.set(counter.get() + 1);
// RefCell: for non-Copy types, runtime borrow check (panics on double borrow)
let cache = RefCell::new(HashMap::new());
cache.borrow_mut().insert("k", 42);
// OnceCell: one-time initialization
static CONFIG: OnceCell<Config> = OnceCell::new();
let cfg = CONFIG.get_or_init(|| Config::load());
| Type |
Sync? |
Use |
Cell<T> |
No |
Copy types only, simple mutation |
RefCell<T> |
No |
Non-Copy types, single-thread |
OnceCell<T> |
No |
One-time lazy init |
OnceLock<T> |
Yes |
One-time lazy init, multi-thread (std 1.70+) |
LazyLock<T> |
Yes |
Transparent lazy init (std 1.80+) |
See references/interior-mutability.md for panic-safety (RefCell double-borrow), choose-Cell-vs-RefCell, and Atomic* for multi-thread.
Part 5: Option and Result Combinators
Option combinators
// map: transform inner value
name.map(|n| n.to_uppercase()) // Option<String>
// and_then: chain fallible operations
id.and_then(|i| db.lookup(i)) // Option<User>
// unwrap_or / unwrap_or_else: provide default
port.unwrap_or(8080)
port.unwrap_or_else(|| env::var("PORT").unwrap_or("8080").parse().unwrap())
// ok_or / ok_or_else: convert Option to Result
user.ok_or(Error::NotFound)?
user.ok_or_else(|| Error::UserMissing(id))?
// filter, is_some, is_some_and
age.filter(|&a| a >= 18)
opt.is_some_and(|x| x > 0)
// transpose: swap Option<Result> → Result<Option>
let x: Result<Option<i32>, _> = Some("5").parse::<i32>().map(Some);
Result combinators
// ? for propagation (prefer over explicit match)
fn load() -> Result<Config, Error> { Ok(parse(file_read()?)?) }
// map_err: convert error type
file_read().map_err(|e| Error::Io(e))?
// map: transform success value
parse(s).map(|n| n * 2)
// and_then: chain fallible
validate(input).and_then(process)
// or_else / or: fallback on error
primary.or_else(|_| fallback())
Common patterns
| Pattern |
Code |
| Try parsing, fallback to default |
s.parse().unwrap_or(default) |
| Early return on error |
let x = result?; |
| Convert Option to Result |
opt.ok_or(Error::Missing)? |
| Collect all Results |
results.collect::<Result<Vec<>, >>() |
| Try all, return first success |
`opts.intoiter().findmap( |
o |
o.try_it().ok())` |
See references/option-result-combinators.md for the full combinator table and refactor patterns.
Part 6: I/O Streams
Trait hierarchy
Read ────┐
├─ BufRead (adds buffering, lines())
Write ───┘
Seek (random access)
Choosing
| Need |
Use |
| Read bytes from a source |
impl Read (File, Cursor, stdin) |
| Buffered reading (lines, until) |
impl BufRead (BufReader wrapping) |
| Write bytes to a destination |
impl Write (File, stdout, Vec) |
| Random access |
impl Seek (File) |
| Async I/O |
tokio::io::AsyncRead/AsyncWrite (rust-concurrency) |
use std::io::{BufRead, BufReader, Read, Write};
use std::fs::File;
let f = File::open("log.txt")?; // impl Read
let reader = BufReader::new(f); // wrap for BufRead
for line in reader.lines() { // BufRead::lines
println!("{}", line?);
}
See references/io-streams.md for Cursor<Vec<u8>>, error handling (io::Result), and converting between sync and async.
Part 7: Iterators
Choosing
| Need |
Use |
| Borrow elements |
.iter() → Iterator<Item = &T> |
| Mutably borrow |
.iter_mut() → Iterator<Item = &mut T> |
| Consume |
.into_iter() → Iterator<Item = T> |
| Index pairs |
.iter().enumerate() |
| Filter |
`.filter(\ |
x\ |
*x > 0)` |
| Transform |
`.map(\ |
x\ |
x * 2)` |
| Flat |
`.flat_map(\ |
x\ |
x.iter())` |
| Collect to Vec |
.collect::<Vec<_>>() |
| Collect to HashMap |
.collect::<HashMap<, >>() (from (K, V) tuples) |
| First match |
`.find(\ |
x\ |
*x == 5)` |
| Any/all |
`.any(\ |
x\ |
*x > 0), .all(\ |
x\ |
*x > 0)` |
| Group/sum |
.sum::<i32>(), .product(), `.fold(0, \ |
a, b\ |
a + b)` |
// Borrowed iteration is the default
let v = vec![1, 2, 3];
for x in &v { /* x: &i32 */ } // .iter()
for x in &mut v { /* x: &mut i32 */ }
for x in v { /* x: i32, v consumed */ }
See references/iterators.md for custom iterators (impl Iterator), lazy evaluation, and zero-cost abstractions.
Part 8: Std Concurrency
The standard library provides threads, channels, and sync primitives. For async, see rust-concurrency.
Threads
use std::thread;
use std::time::Duration;
let handle = thread::spawn(|| {
thread::sleep(Duration::from_millis(500));
"result"
});
let r = handle.join().unwrap(); // wait, get result
Scoped threads (Rust 1.63+) — borrow without 'static
let mut data = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| { data[0] += 1; });
s.spawn(|| { data[1] += 1; });
}); // all threads joined here; data borrow ends
mpsc channels
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
println!("{}", rx.recv().unwrap());
Sync primitives
| Primitive |
Use |
Mutex<T> |
Exclusive lock, sync interior mutability |
RwLock<T> |
Many readers, few writers |
Arc<T> |
Atomic reference counting |
Barrier |
Synchronize multiple threads at a point |
Once |
One-time initialization |
Condvar |
Wait/notify |
Atomic* (AtomicUsize, AtomicBool, ...) |
Lock-free counters, flags |
See references/std-concurrency.md for Arc<Mutex<T>> patterns, poison recovery, and when to graduate to Tokio.
Part 9: Time, Path, Process
Time
use std::time::{Duration, Instant, SystemTime};
let start = Instant::now();
expensive_op();
println!("took {:?}", start.elapsed()); // Duration
let five_secs = Duration::from_secs(5);
let mixed = Duration::new(5, 500_000_000); // 5.5s
let now = SystemTime::now();
let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH)?;
Duration — span of time (no specific moment)
Instant — monotonic moment, for measuring elapsed
SystemTime — wall-clock, for timestamps (can go backwards!)
Path
use std::path::{Path, PathBuf};
let p = Path::new("/usr/bin/foo");
let stem = p.file_stem(); // "foo"
let ext = p.extension(); // "foo" → None; "foo.txt" → "txt"
let joined = PathBuf::from("/usr").join("bin").join("foo");
let parent = p.parent(); // "/usr/bin"
Always use Path/PathBuf for filesystem paths — never String. Cross-platform safe.
Process
use std::process::Command;
let output = Command::new("ls")
.arg("-l")
.arg("/")
.output()?; // waits, returns Output
println!("{}", String::from_utf8_lossy(&output.stdout));
// Stream stdin/stdout
use std::process::Stdio;
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
See references/process-and-fs.md for Command exit codes, signal handling, and fs module (read, write, create_dir, metadata).
Workflow
- Identify the need — collection, pointer, string, I/O, iterator, or system interface?
- Apply decision tree — pick from the relevant Part above
- Check MSRV — some APIs (
OnceLock 1.70, LazyLock 1.80, scoped threads 1.63) need recent stable
- Compose with combinators — prefer
Option/Result chains over explicit match
- Verify —
cargo check, cargo test, cargo clippy
- Hand off — async runtime →
rust-concurrency; unsafe → rust-unsafe-ffi; application concerns → domain skills
Decision Shortcuts
| Question |
Answer |
| Default collection? |
Vec<T> |
| Need key-value lookup? |
HashMap (unordered) or BTreeMap (sorted) |
| Shared ownership? |
Rc (single-thread) or Arc (multi-thread) |
| Mutable shared state? |
RefCell (single) or Mutex/RwLock (multi) |
| String parameter? |
&str always |
| File path? |
Path / PathBuf |
| Lazy global? |
OnceLock (1.70+) or LazyLock (1.80+) |
| Iterate borrowed? |
.iter() |
| Two-way queue? |
VecDeque |
| Priority queue? |
BinaryHeap |
Resources
- [Collections Deep Dive](references/collections.md)
- [Smart Pointers](references/smart-pointers.md)
- [String Types](references/string-types.md)
- [Option/Result Combinators](references/option-result-combinators.md)
- [I/O Streams](references/io-streams.md)
- [Iterators](references/iterators.md)
- [Std Concurrency](references/std-concurrency.md)
- [Process and Filesystem](references/process-and-fs.md)
- [Interior Mutability](references/interior-mutability.md)
- [Std Module Index](references/std-module-index.md)
examples/golden-stdlib/: a crate exercising collections, pointers, channels, and iterators
Upstream Sources