#211 Jun 2026

211. Iterator::inspect — Debug a Lazy Chain Without Tearing It Apart

When a map/filter chain gives the wrong answer, the reflex is to rip it into a for loop just to drop in a println!. You don’t have to — inspect lets you peek at every value as it flows past.

inspect runs a closure on each item and then hands the same item downstream, untouched. It’s a tap on the pipe: perfect for seeing what survives each stage without restructuring anything.

Say this sum is wrong and you want to know which numbers actually make it through the filter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let nums = [1, 2, 3, 4, 5, 6];

let sum: i32 = nums
    .iter()
    .filter(|&&n| n % 2 == 0)
    .inspect(|n| println!("kept: {n}"))
    .map(|&n| n * 10)
    .inspect(|n| println!("scaled: {n}"))
    .sum();

assert_eq!(sum, 120); // (2 + 4 + 6) * 10

You can drop a tap between any two adapters, as many as you like, and the result is identical to the chain without them. Because iterators are lazy, the closures fire only as items are pulled — so the prints interleave with the real work instead of dumping everything up front.

The key property is that inspect is transparent: it borrows each item (&T) and returns the iterator unchanged, so types and values flow through exactly as before:

1
2
3
4
5
6
let evens: Vec<i32> = (1..=6)
    .inspect(|n| eprintln!("saw: {n}"))
    .filter(|n| n % 2 == 0)
    .collect();

assert_eq!(evens, [2, 4, 6]);

Reach for eprintln! inside the closure so debug output goes to stderr and stays out of any data you’re printing to stdout. And when you’re done, deleting the tap is a one-line change — not an unwind of the loop you’d otherwise have written.

210. next_power_of_two — Round Up to a Power of Two Without the Bit-Twiddling

Sizing a buffer or hash table to the next power of two is a classic — and people keep reinventing it with shifts and leading_zeros. The standard library already has it.

You’ve probably seen (or written) the bit-twiddling version, which is easy to get subtly wrong on edge cases like 0 or exact powers:

1
2
3
4
5
6
7
8
9
fn round_up_manual(n: u32) -> u32 {
    let mut v = n - 1;        // underflows when n == 0
    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    v + 1
}

next_power_of_two does exactly this, correctly, for every unsigned integer type:

1
2
3
4
assert_eq!(5u32.next_power_of_two(), 8);
assert_eq!(8u32.next_power_of_two(), 8);   // already a power of two
assert_eq!(1u32.next_power_of_two(), 1);
assert_eq!(0u32.next_power_of_two(), 1);   // the tricky one, handled

A common use is rounding an allocation up so masking can replace modulo:

1
2
3
4
5
6
7
let requested = 100usize;
let cap = requested.next_power_of_two();   // 128
assert_eq!(cap, 128);

// because cap is a power of two, idx % cap == idx & (cap - 1)
let idx = 1234usize;
assert_eq!(idx % cap, idx & (cap - 1));

The one trap: if the next power of two doesn’t fit in the type, next_power_of_two panics in debug and wraps to 0 in release. When the input is untrusted, reach for checked_next_power_of_two, which hands you an Option instead:

1
2
3
assert_eq!(200u8.next_power_of_two(), 0);          // wraps in release — bug waiting to happen
assert_eq!(200u8.checked_next_power_of_two(), None); // explicit, safe
assert_eq!(100u8.checked_next_power_of_two(), Some(128));
#209 Jun 2026

209. ..Default::default() — set the fields you care about, default the rest

Building a struct with a dozen fields when you only want to change one is tedious. Struct update syntax lets you fill in the rest from a default — or from any other value.

Say you have a config struct. Spelling out every field just to flip one flag is noise:

1
2
3
4
5
6
7
let cfg = ServerConfig {
    host: String::new(),
    port: 8080,
    max_connections: 0,
    use_tls: true,
    timeout_secs: 0,
};

Derive Default, then use ..Default::default() to fill the fields you didn’t mention:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug, Default, PartialEq)]
struct ServerConfig {
    host: String,
    port: u16,
    max_connections: u32,
    use_tls: bool,
    timeout_secs: u64,
}

let cfg = ServerConfig {
    port: 8080,
    use_tls: true,
    ..Default::default()
};

assert_eq!(cfg.port, 8080);
assert_eq!(cfg.use_tls, true);
assert_eq!(cfg.host, "");        // String::default()
assert_eq!(cfg.max_connections, 0);

The ..base part isn’t limited to Default — you can spread from any existing instance, so it doubles as a cheap “clone but tweak”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let base = ServerConfig {
    max_connections: 1024,
    ..Default::default()
};

let derived = ServerConfig {
    port: 9090,
    ..base
};

assert_eq!(derived.port, 9090);
assert_eq!(derived.max_connections, 1024);

Two things to remember: the ..rest has to come last, and the fields it pulls in are moved out of the source value (or copied, if they’re Copy) — so a non-Copy field like host means you can’t keep using base afterward.

208. f64::mul_add — One Rounding, One Instruction, Better Accuracy

a * b + c rounds twice and may compile to two separate operations. a.mul_add(b, c) computes the whole thing at full precision, rounds once, and on a CPU with FMA folds into a single instruction.

Two roundings vs. one

Floating-point math rounds after every operation. Writing a * b + c first rounds the product a * b to the nearest f64, then rounds again after adding c. That intermediate rounding throws away bits before they ever reach the sum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let a = 1.0_f64;
let b = 2.0_f64;
let c = 3.0_f64;

// Two roundings: (a*b) rounded, then (+ c) rounded
let separate = a * b + c;

// One rounding: a*b + c evaluated at full precision, rounded once
let fused = a.mul_add(b, c);

assert_eq!(separate, 5.0);
assert_eq!(fused, 5.0);

For tidy values they agree. The difference shows up when the product needs more bits than an f64 can hold and the addition would have recovered them — exactly the catastrophic-cancellation cases that wreck numeric code.

Where it pays off: Horner’s method

Polynomial evaluation is built out of multiply-then-add, so it’s the textbook home for mul_add. To evaluate 2x² + 3x + 4 with Horner’s method you nest the operations, and each step is one fused step:

1
2
3
4
5
6
fn poly(x: f64) -> f64 {
    // ((2*x) + 3)*x + 4
    2.0_f64.mul_add(x, 3.0).mul_add(x, 4.0)
}

assert_eq!(poly(2.0), 18.0); // 2*4 + 3*2 + 4

Each mul_add keeps full precision through the chain instead of rounding at every * and +. The same pattern carries dot products, which are a sum of products:

1
2
3
4
5
6
7
fn dot(a: &[f64], b: &[f64]) -> f64 {
    a.iter()
        .zip(b)
        .fold(0.0, |acc, (&x, &y)| x.mul_add(y, acc))
}

assert_eq!(dot(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]), 32.0);

The honest caveat

mul_add is faster only when the target CPU has a hardware fused-multiply-add instruction (modern x86-64 with FMA, AArch64, most others you’ll deploy to). Where it doesn’t, the standard library has to emulate the exact single-rounding semantics in software, and that emulation is slower than a plain a * b + c. So this is a hot-path tool: reach for it in tight numeric loops on hardware you control (or behind target-feature/target-cpu flags), and lean on it freely when you need the extra accuracy regardless of speed.

The bottom line

mul_add gives you a single rounding step and, on capable hardware, a single instruction for a * b + c. Use it in polynomial evaluation, dot products, and any multiply-accumulate loop where precision or throughput matters — and remember it can regress on FMA-less targets, so measure when speed is the goal.

207. if let Guards — Match a Pattern Inside a Guard, and Still Fall Through

A match arm whose extra check is itself a fallible if let used to force you to nest a second match — and duplicate the fallback. Rust 1.95 lets the guard do the binding.

Say a config field is a raw string and you want to parse it as a port, defaulting when it’s absent or garbage:

1
2
3
4
enum Field {
    Raw(String),
    Missing,
}

Before if let guards, a guard could only hold a bool expression — it couldn’t bind. So the parse had to move inside the arm body, and the fallback ended up written twice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn to_port(f: &Field) -> u16 {
    match f {
        Field::Raw(s) => {
            if let Ok(p) = s.parse::<u16>() {
                p
            } else {
                8080 // fallback #1: Raw but unparseable
            }
        }
        Field::Missing => 8080, // fallback #2: same value, second copy
    }
}

Two copies of 8080 that have to stay in sync. The nested if let can’t fall through to another arm — once you’re in Field::Raw, you’re stuck handling everything there.

Since Rust 1.95, if let works directly in the guard. If the pattern fails to match, the arm is skipped and matching continues to the next arm:

1
2
3
4
5
6
fn to_port(f: &Field) -> u16 {
    match f {
        Field::Raw(s) if let Ok(p) = s.parse::<u16>() => p,
        _ => 8080, // one fallback, covers Missing AND unparseable Raw
    }
}

The guard both tests and binds p, and a failed parse simply drops to the catch-all. One fallback, no duplication.

It composes with && too, so you can chain several binds in one guard:

1
2
3
4
5
6
7
8
9
fn first_two(line: &str) -> Option<(i32, i32)> {
    let mut it = line.split(',');
    match (it.next(), it.next()) {
        (Some(a), Some(b))
            if let Ok(x) = a.trim().parse::<i32>()
            && let Ok(y) = b.trim().parse::<i32>() => Some((x, y)),
        _ => None,
    }
}

Reach for it whenever a match arm needs a second, fallible look at its data and you want a clean failure to roll into the next arm instead of a pyramid of nested matches.

#206 Jun 2026

206. trim_ascii — Trim Whitespace From &[u8] Without Going Through str

Got a &[u8] with leading or trailing whitespace? You don’t need to validate it as UTF-8 just to trim it.

The reflex is to convert to str first — which can fail and forces a UTF-8 check you may not want:

1
2
3
4
5
6
let raw: &[u8] = b"  42 \n";

// The detour: validate as UTF-8, trim, go back to bytes
let s = std::str::from_utf8(raw).unwrap(); // can panic on non-UTF-8
let trimmed = s.trim().as_bytes();
assert_eq!(trimmed, b"42");

Since Rust 1.80, byte slices trim themselves directly — no str, no validation, no panic path:

1
2
3
4
5
let raw: &[u8] = b"  42 \n";

assert_eq!(raw.trim_ascii(),       b"42");
assert_eq!(raw.trim_ascii_start(), b"42 \n");
assert_eq!(raw.trim_ascii_end(),   b"  42");

It strips ASCII whitespace (space, tab, newline, carriage return, form feed, vertical tab) from a &[u8] and hands back a borrowed sub-slice — zero allocation.

Best part: it’s a const fn, so it works where str::trim can’t:

1
2
const KEY: &[u8] = b"  secret  ".trim_ascii();
assert_eq!(KEY, b"secret");

Reach for it when you’re parsing bytes straight off a socket, file, or buffer and want to clean up edges before validating the rest.

#205 Jun 2026

205. strip_prefix / strip_suffix — Remove a Prefix Once, Not Every Repeat

Reaching for trim_start_matches to peel off a "--" or a leading slash? It strips every repeated match and silently does nothing when there’s no match. strip_prefix removes exactly one and tells you whether it hit.

The Problem

trim_start_matches keeps eating as long as the pattern matches, which is rarely what “remove the prefix” means:

1
2
3
4
5
6
7
let path = "/////etc";

// Strips ALL leading slashes — not just one
assert_eq!(path.trim_start_matches('/'), "etc");

// And with a repeated substring it does the same
assert_eq!("foofoobar".trim_start_matches("foo"), "bar");

It also can’t tell you whether anything was removed — a non-match returns the string unchanged, so you can’t branch on it.

The Fix: strip_prefix

strip_prefix removes one occurrence and returns an Option: Some(rest) on a hit, None when the prefix isn’t there.

1
2
3
4
5
let path = "/////etc";

assert_eq!(path.strip_prefix('/'), Some("////etc")); // exactly one
assert_eq!("foofoobar".strip_prefix("foo"), Some("foobar"));
assert_eq!("foobar".strip_prefix("xyz"), None);       // no match, told you so

The Option is the real win: it doubles as a “did this start with the prefix?” test. Parsing a CLI flag becomes a one-liner:

1
2
3
4
5
6
fn flag_value(arg: &str) -> Option<&str> {
    arg.strip_prefix("--")
}

assert_eq!(flag_value("--verbose"), Some("verbose"));
assert_eq!(flag_value("positional"), None);

Its Mirror: strip_suffix

Same deal at the other end — perfect for trimming a known extension or unit without slicing indices by hand:

1
2
3
4
5
6
fn without_ext(name: &str) -> &str {
    name.strip_suffix(".rs").unwrap_or(name)
}

assert_eq!(without_ext("main.rs"), "main");
assert_eq!(without_ext("README"), "README"); // unchanged, no panic

Use trim_start_matches / trim_end_matches only when you genuinely want to collapse a run of repeats. For peeling one known prefix or suffix — and knowing if it was there — strip_prefix and strip_suffix say exactly what you mean.

#204 Jun 2026

204. take_while / skip_while — Act on the Leading Run, Not Every Match

Reaching for .filter() to drop leading blank lines? It’ll drop the blank lines in the middle too. take_while and skip_while work on the leading run and stop the moment the predicate flips.

The Problem

You want everything after the leading comment/blank lines in a config — but filter has no concept of “leading”:

1
2
3
4
5
6
7
8
9
let lines = ["# header", "", "key = 1", "", "key = 2"];

// Wrong: this also eats the blank line between the two keys
let body: Vec<&str> = lines
    .iter()
    .copied()
    .filter(|l| !l.is_empty() && !l.starts_with('#'))
    .collect();
assert_eq!(body, ["key = 1", "key = 2"]); // lost the structure

filter tests every element independently, so it strips matches wherever they appear. That’s rarely what “skip the header” means.

The Fix: skip_while

skip_while discards elements while the predicate holds, then yields the rest untouched — including later elements that would have matched:

1
2
3
4
5
6
7
8
let lines = ["# header", "", "key = 1", "", "key = 2"];

let body: Vec<&str> = lines
    .iter()
    .copied()
    .skip_while(|l| l.is_empty() || l.starts_with('#'))
    .collect();
assert_eq!(body, ["key = 1", "", "key = 2"]); // blank line kept

The blank line between the keys survives because skip_while already stopped skipping at "key = 1".

Its Mirror: take_while

take_while yields the leading run and stops at the first non-match — perfect for parsing a prefix:

1
2
3
4
5
6
7
let input = "42px";

let digits: String = input.chars().take_while(|c| c.is_ascii_digit()).collect();
let unit: String = input.chars().skip_while(|c| c.is_ascii_digit()).collect();

assert_eq!(digits, "42");
assert_eq!(unit, "px");

take_while halts at 'p', so even a trailing "9" in the unit wouldn’t sneak back into digits. Unlike filter, both adapters care about position: they describe the boundary between a leading run and everything after it.

203. Peekable::next_if_map — Consume a Token Only If It Parses, Transform in One Step

next_if only answers yes/no, so when you also need the converted value you end up peeking, computing, and calling next() by hand. Rust 1.94 stabilized Peekable::next_if_map — conditionally consume the next item and transform it in a single call, putting the item back if it doesn’t match.

The trap: the peek / compute / advance dance

Hand-rolled lexers are full of this pattern — look at the next item, decide whether it’s the kind you want, and only then consume it. With next_if you can express the decide part, but next_if hands you back the original item, so you have to redo the conversion afterward. Most people skip it and drop down to a manual peek() + next():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::iter::Peekable;
use std::str::Chars;

// Peek, compute the digit, THEN remember to advance. Three steps,
// and it's easy to forget the next() and loop forever.
fn take_digit_manual(it: &mut Peekable<Chars>) -> Option<u32> {
    let &c = it.peek()?;
    let d = c.to_digit(10)?;
    it.next();
    Some(d)
}

The conversion (to_digit) and the consumption (next) are split across separate lines, and the iterator only advances as a side effect. Forget the it.next() and you’ve written an infinite loop.

The fix: decide and transform in one call

next_if_map takes the next item by value and hands it to a closure returning Result<R, Item>. Return Ok(value) and the item is consumed, giving you Some(value); return Err(item) and the item is pushed back, giving you None. The classic conversion-or-give-back is just .ok_or(c):

1
2
3
4
5
6
use std::iter::Peekable;
use std::str::Chars;

fn take_digit(it: &mut Peekable<Chars>) -> Option<u32> {
    it.next_if_map(|c| c.to_digit(10).ok_or(c))
}

One line, no manual next(), and the “advance only on success” rule is enforced by the method instead of by you remembering to call it.

It really does put the item back

When the closure returns Err, the iterator is left exactly where it was — the next read still sees that item:

1
2
3
4
5
6
7
8
9
# use std::iter::Peekable;
# use std::str::Chars;
# fn take_digit(it: &mut Peekable<Chars>) -> Option<u32> {
#     it.next_if_map(|c| c.to_digit(10).ok_or(c))
# }
let mut it = "px".chars().peekable();

assert_eq!(take_digit(&mut it), None); // 'p' isn't a digit...
assert_eq!(it.next(), Some('p'));      // ...so it's still here

That give-it-back guarantee is what makes it safe to chain in a loop: each call either makes progress or leaves the stream untouched for the next rule to try.

Where it shines: tokenizing

A digit-run parser becomes a tight while let that stops cleanly at the first non-digit, leaving the rest of the input for whatever comes next:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn parse_number(s: &str) -> (u64, String) {
    let mut it = s.chars().peekable();
    let mut n = 0u64;
    while let Some(d) = it.next_if_map(|c| c.to_digit(10).ok_or(c)) {
        n = n * 10 + d as u64;
    }
    (n, it.collect()) // leftover chars, untouched
}

assert_eq!(parse_number("42px"), (42, "px".to_string()));
assert_eq!(parse_number("2026"), (2026, String::new()));

There’s also next_if_map_mut, which passes &mut Item and takes a closure returning Option<R> — handy when the item is expensive to move or you want to mutate it in place rather than hand it back.

The bottom line

When you only want the next item if it converts to something useful, reach for next_if_map instead of the peek / compute / next shuffle. It folds the test and the transform into one call and guarantees the iterator only advances when the conversion succeeds — exactly the invariant hand-written lexers keep getting wrong.

202. Arc::clone Is a Refcount Bump, Not a Deep Copy — Share Big Data, Don't Duplicate It

big.clone() on a 50MB lookup table allocates 50MB every time a worker needs a copy. Wrap it in an Arc once and Arc::clone is just an atomic +1 on a counter — every owner reads the same bytes.

This closes out performance week, the afternoon pair to the morning’s entry() bite: that one was about not building a default you’ll throw away, this one is about not copying a payload you only ever read.

The trap: .clone() deep-copies the payload

When several owners each need “their own” handle to a large immutable value, the obvious move is to clone it. But Clone on a Vec/String/HashMap walks the data and allocates a fresh copy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let table: Vec<u64> = (0..1_000_000).collect();

// Four "owners" — four full heap allocations, ~32MB copied.
let a = table.clone();
let b = table.clone();
let c = table.clone();

assert_eq!(a.len(), 1_000_000);
assert_eq!(b[500_000], 500_000);
assert_eq!(c.last(), Some(&999_999));

Nobody mutates the data — they just read it — yet you paid for four independent copies. That’s pure waste.

The fix: one allocation, shared by reference count

Put the value behind an Arc<T> once. Now Arc::clone doesn’t touch the payload at all — it bumps an atomic reference count and hands back another pointer to the same allocation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::sync::Arc;

let table: Arc<Vec<u64>> = Arc::new((0..1_000_000).collect());

let a = Arc::clone(&table);
let b = Arc::clone(&table);
let c = Arc::clone(&table);

// All four point at the SAME bytes — no data was copied.
assert_eq!(Arc::strong_count(&table), 4);
assert_eq!(a[500_000], 500_000);

// Proof it's one allocation, not four:
assert!(Arc::ptr_eq(&a, &b));
assert!(Arc::ptr_eq(&b, &c));

The Arc::clone(&x) spelling (rather than x.clone()) is a convention worth keeping: at the call site it reads as “bump the counter,” so a reviewer knows a cheap pointer copy happened, not a 32MB memcpy.

This is what makes it cheap to send to threads

The same property is why Arc is the building block for sharing immutable data across threads — each thread gets a counted handle, all reading one copy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::sync::Arc;
use std::thread;

let config: Arc<Vec<u64>> = Arc::new((0..1_000).collect());

let handles: Vec<_> = (0..4)
    .map(|_| {
        let c = Arc::clone(&config); // refcount bump, then moved into the thread
        thread::spawn(move || c.iter().sum::<u64>())
    })
    .collect();

let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
assert_eq!(total, 499_500 * 4);

Four threads, one underlying Vec. Cloning the payload into each thread would have been four allocations; Arc::clone is four counter bumps.

When not to reach for it

Arc shines for data that’s large, shared, and read-only after construction. It’s not free: every clone and drop is an atomic operation, and the payload lives until the last handle goes away. For small Copy types (bite-200) a plain copy is cheaper than the atomic traffic. And if you need to mutate shared state, you want Arc<Mutex<T>> or Arc<RwLock<T>> (bite-160) — or Arc::make_mut (bite-113) for copy-on-write.

The bottom line

If you’re cloning a big value just to hand read-only access to several owners or threads, you’re copying bytes nobody changes. Wrap it in Arc once: every Arc::clone after that is an atomic increment over a shared allocation, not a deep copy.