20 / 20 · Day 7
Day 7 · Concept 20

Idiomatic Rust

The ten patterns that separate "I know the language" from "I write Rust". Iterator chains over loops; ? over match; newtypes over primitives; clippy as your style guide.


1 · Iterator chains over loops

rust src/main.rs
// Less idiomatic
let mut squares = Vec::new();
for n in &nums {
    if *n > 0 {
        squares.push(n * n);
    }
}

// More idiomatic
let squares: Vec<_> = nums.iter()
    .filter(|n| **n > 0)
    .map(|n| n * n)
    .collect();

Reads top-to-bottom as a pipeline. Same compiled output (LLVM is good at this), tighter source.

2 · ? everywhere; unwrap nowhere

rust src/main.rs
// Outside tests, .unwrap() is a code smell.
let cfg = load_config()?;        // good
let cfg = load_config().unwrap(); // bad
let cfg = load_config().expect("config must exist"); // ok at boundaries

? propagates; expect("...") documents the assumption when you really must panic.

3 · Newtype over primitive

rust src/main.rs · type the domain
// Easy to mix up
fn transfer(from: u64, to: u64, amount: u64) { /* ... */ }

// Hard to misuse
struct UserId(u64);
struct Cents(u64);
fn transfer(from: UserId, to: UserId, amount: Cents) { /* ... */ }

Compiler now rejects transfer(amount, sender, receiver) at the call site.

4 · Take &str, return String

Inputs that are read-only borrow (&str, &[T]); outputs that the caller owns are returned by value (String, Vec<T>). This lets callers pass owned or borrowed data without conversions.

5 · derive aggressively

rust src/main.rs
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    id: u64,
    name: String,
}

Free implementations of the standard traits. Add Default if every field has a default; Serialize/Deserialize from serde for JSON.

6 · Builder pattern for many optional params

rust src/main.rs
Request::new("/users")
    .method(Method::POST)
    .timeout_ms(500)
    .body(payload)
    .send()
    .await?;

Beats functions with 9 parameters. Crates like derive_builder generate the boilerplate.

7 · clippy is the style guide

shell terminal
$ cargo clippy --all-targets -- -D warnings

Treat clippy lints as errors in CI. Many are pure style; many catch real bugs. Apply #[allow(clippy::lint_name)] only when you have a reason.

8 · Avoid unsafe unless you must

Unsafe Rust exists for FFI, low-level data structures, and performance work. For application code, you almost never need it. When you do, isolate to a single function and document the invariants.

9 · Trait bounds, not concrete types

rust src/main.rs
// Less flexible
fn print_lines(s: String) { /* ... */ }

// More flexible
fn print_lines(s: impl AsRef<str>) { /* ... */ }

// Accepts &str, String, Cow<str>, etc.

10 · When it really clicks

  • You read compiler errors before reaching for Stack Overflow.
  • You design the API by deciding the ownership and borrow shapes first.
  • You're glad — not annoyed — when the borrow checker rejects something.
Found this useful?