#231 Jun 2026

231. slice::chunk_by — Group Consecutive Elements Without the Index Bookkeeping

Splitting a slice into runs of equal (or related) elements usually means a manual loop with a start index and an off-by-one waiting to happen. slice::chunk_by does it in one call.

You want to break [1, 1, 2, 3, 3, 3, 1] into its runs of equal values. The hand-rolled version tracks where each run starts and has to remember to flush the last one:

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

let mut groups: Vec<&[i32]> = Vec::new();
let mut start = 0;
for i in 1..=data.len() {
    if i == data.len() || data[i] != data[i - 1] {
        groups.push(&data[start..i]);
        start = i;
    }
}

chunk_by takes a predicate over consecutive pairs and starts a new chunk whenever it returns false:

1
2
3
4
5
6
7
8
let data = [1, 1, 2, 3, 3, 3, 1];

let groups: Vec<&[i32]> = data.chunk_by(|a, b| a == b).collect();

assert_eq!(
    groups,
    [&[1, 1][..], &[2], &[3, 3, 3], &[1]],
);

The predicate is any relation between neighbours, not just equality. Want the ascending runs of a sequence? Split where the order breaks:

1
2
3
4
5
let nums = [1, 3, 5, 2, 4, 1];

let runs: Vec<&[i32]> = nums.chunk_by(|a, b| a <= b).collect();

assert_eq!(runs, [&[1, 3, 5][..], &[2, 4], &[1]]);

Each chunk is a borrowed &[T] into the original slice — no copying. There’s a chunk_by_mut for when you need &mut [T] slices instead. (Stable since Rust 1.77; it was briefly named group_by.)

#230 Jun 2026

230. slice::split_at_mut — Two Mutable Halves, No unsafe, No Borrow Fight

Need a &mut to two different elements of the same slice at once? Indexing twice won’t compile — the borrow checker sees the whole slice borrowed twice. split_at_mut hands you two non-overlapping mutable halves instead.

The borrow checker says no

You want to mutate two elements of a slice in the same expression — say, swap-style logic that reads one and writes another:

1
2
3
4
let mut v = [1, 2, 3, 4];
let a = &mut v[0];
let b = &mut v[3]; // error: cannot borrow `v` as mutable more than once
*a += *b;

The compiler can’t prove v[0] and v[3] are different elements, so it rejects two overlapping &mut borrows of v. Both indexing operations borrow the entire slice.

split_at_mut gives you two disjoint slices

split_at_mut(mid) splits a slice into [0, mid) and [mid, len), returning a &mut to each. Because the halves provably don’t overlap, the borrow checker is happy:

1
2
3
4
let mut v = [1, 2, 3, 4];
let (left, right) = v.split_at_mut(2);
left[0] += right[1]; // v[0] += v[3]
assert_eq!(v, [5, 2, 3, 4]);

Internally it’s one bounds check and a pointer offset — no copying, no allocation. It panics only if mid > len; use split_at_mut_checked to get an Option instead.

Where it shines: in-place algorithms

Reversing a slice by hand needs a read and a write to two indices each step. Split once, walk inward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn reverse<T>(s: &mut [T]) {
    let n = s.len();
    let (front, back) = s.split_at_mut(n / 2);
    for (a, b) in front.iter_mut().zip(back.iter_mut().rev()) {
        std::mem::swap(a, b);
    }
}

let mut data = [1, 2, 3, 4, 5];
reverse(&mut data);
assert_eq!(data, [5, 4, 3, 2, 1]);

The two iterators borrow disjoint halves, so iterating both mutably at once just works.

Divide and conquer for free

Because each half is itself a &mut [T], recursive algorithms split cleanly — the basis for parallelizing with something like Rayon’s join:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn sum_halves(s: &mut [i32]) -> i32 {
    if s.len() <= 1 {
        return s.first().copied().unwrap_or(0);
    }
    let mid = s.len() / 2;
    let (left, right) = s.split_at_mut(mid);
    sum_halves(left) + sum_halves(right)
}

let mut v = [1, 2, 3, 4, 5];
assert_eq!(sum_halves(&mut v), 15);

Whenever you’re fighting the borrow checker over two mutable spots in one slice, stop reaching for unsafesplit_at_mut is the safe, zero-cost answer. For non-adjacent individual elements, its cousin get_disjoint_mut does the same trick by index.

#229 Jun 2026

229. char::to_digit — Turn a Digit Character Into Its Value, Not Its Byte

Reached for c as u8 - b'0' to turn '7' into 7? It works until the input isn’t a digit — then you get silent garbage. char::to_digit does it safely and handles hex too.

The byte-math trap

The classic trick subtracts the ASCII code of '0':

1
2
3
let c = '7';
let value = c as u8 - b'0';
assert_eq!(value, 7); // fine...

It’s fine for '0'..='9'. But there’s no validation — feed it anything else and the arithmetic just keeps going:

1
2
3
4
5
// 'f' is 102, b'0' is 48 → 54, which is nonsense, not 15
assert_eq!('f' as u8 - b'0', 54);

// 'a' is 97 → 97 - 48 = 49, also wrong
assert_eq!('a' as u8 - b'0', 49);

No panic, no None — just a wrong number flowing downstream.

to_digit returns an Option

char::to_digit(radix) converts a digit character to its numeric value and gives you an Option<u32>, so non-digits become None instead of garbage:

1
2
3
assert_eq!('7'.to_digit(10), Some(7));
assert_eq!('0'.to_digit(10), Some(0));
assert_eq!('x'.to_digit(10), None); // not a digit → None

Pass 16 and it parses hex, letters included and case-insensitive:

1
2
3
4
assert_eq!('f'.to_digit(16), Some(15));
assert_eq!('F'.to_digit(16), Some(15));
assert_eq!('a'.to_digit(16), Some(10));
assert_eq!('9'.to_digit(16), Some(9));

Where it shines

Because it returns an Option, it drops straight into filter_map — sum every digit in a string and skip everything else, no manual bounds check:

1
2
let total: u32 = "a1b2c3".chars().filter_map(|c| c.to_digit(10)).sum();
assert_eq!(total, 6);

The reverse direction

char::from_digit goes the other way — a value plus a radix back to a digit character:

1
2
3
assert_eq!(char::from_digit(15, 16), Some('f'));
assert_eq!(char::from_digit(9, 10), Some('9'));
assert_eq!(char::from_digit(42, 16), None); // out of range → None

Both have been stable since Rust 1.0. Whenever you’re tempted to do char-to-number arithmetic by hand, reach for to_digit — it validates, it does hex, and it never silently lies to you.

#228 Jun 2026

228. Vec::push_mut — Push and Get a &mut Back, Skip the last_mut().unwrap()

Pushed an element and immediately needed to tweak it? The old way was push then last_mut().unwrap(). Rust 1.95 added push_mut, which hands you the &mut right back.

The push-then-refetch dance

You add an element to a Vec, then need a mutable handle to it — to set a field, run a builder step, whatever. push returns (), so you go fishing for what you just put in:

1
2
3
4
5
6
7
struct Span { start: usize, end: usize }

let mut spans = Vec::new();
spans.push(Span { start: 0, end: 0 });
let last = spans.last_mut().unwrap(); // re-fetch what we just pushed
last.end = 10;
assert_eq!(spans[0].end, 10);

That last_mut().unwrap() is pure ceremony. You know it’s there — you pushed it one line ago — but the type system makes you unwrap an Option anyway.

push_mut returns the reference directly

Stabilized in Rust 1.95, Vec::push_mut appends the value and returns a &mut T to it in one step:

1
2
3
4
5
6
struct Span { start: usize, end: usize }

let mut spans = Vec::new();
let last = spans.push_mut(Span { start: 0, end: 0 });
last.end = 10;
assert_eq!(spans[0].end, 10);

No Option, no unwrap, no second lookup. The borrow checker is happy because the returned reference is tied to the Vec.

Where it shines: build-then-mutate loops

Parsing or accumulating state where each new entry gets refined before you move on reads much cleaner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[derive(Debug, PartialEq)]
struct Token { text: String, len: usize }

let mut tokens = Vec::new();
for word in "fn main".split_whitespace() {
    let tok = tokens.push_mut(Token { text: word.to_string(), len: 0 });
    tok.len = tok.text.len(); // fill in a derived field in place
}

assert_eq!(tokens[0], Token { text: "fn".into(), len: 2 });
assert_eq!(tokens[1], Token { text: "main".into(), len: 4 });

Not just Vec

The same _mut family landed across the collections: Vec::insert_mut, VecDeque::push_front_mut / push_back_mut / insert_mut, and LinkedList::push_front_mut / push_back_mut all return a &mut to the freshly inserted element:

1
2
3
4
5
6
use std::collections::VecDeque;

let mut q = VecDeque::new();
let front = q.push_front_mut(1);
*front += 41;
assert_eq!(q[0], 42);

Whenever you push and immediately need to touch what you pushed, reach for push_mut — the reference is already in your hand.

#227 Jun 2026

227. trim_matches — Strip the Same Char Off Both Ends, However Many There Are

trim() only knows about whitespace, and strip_prefix peels off one occurrence. When you need to shave every leading and trailing . (or 0, or quote) off a string, reach for trim_matches.

The hand-rolled trim

You’ve got a string padded with some character and want it gone from both ends — but only the ends:

1
2
3
4
5
let s = "***heading***";

// strip_prefix only removes one, and only the front
let once = s.strip_prefix('*').unwrap_or(s);
assert_eq!(once, "**heading***");

Looping strip_prefix/strip_suffix until they stop matching works, but it’s a chore. trim_matches does exactly that for you — it removes all consecutive matches from both ends and leaves the middle alone:

1
2
3
4
5
let s = "***heading***";
assert_eq!(s.trim_matches('*'), "heading");

// only the ends — interior matches stay put
assert_eq!("0x00ff00".trim_matches('0'), "x00ff");

One end at a time

There are directional versions when you only care about one side:

1
2
assert_eq!("--verbose".trim_start_matches("--"), "verbose");
assert_eq!("file.txt.bak".trim_end_matches(".bak"), "file.txt");

The pattern can be a closure or a set of chars

The argument is a Pattern, so you’re not limited to a single char. Pass a closure to trim by predicate, or an array of chars to trim any of them:

1
2
3
4
5
// strip leading digits
assert_eq!("12abc34".trim_start_matches(|c: char| c.is_numeric()), "abc34");

// trim any of several characters
assert_eq!("(value)".trim_matches(['(', ')']), "value");

Note trim_end_matches("--") strips the whole substring repeatedly, not a set of chars — that’s the difference between passing "--" and passing ['-'].

#226 Jun 2026

226. unwrap_or_default — Stop Spelling Out the Empty Value

Writing .unwrap_or(0) or .unwrap_or_else(String::new) to fall back to an empty value? If the type already has a Default, unwrap_or_default says it for you.

The fallback you keep typing out

You pull a value out of an Option, and the “missing” case is just the type’s natural zero: 0 for a number, "" for a string, [] for a vec. So you spell it out:

1
2
3
4
5
6
7
let count: Option<u32> = None;
let n = count.unwrap_or(0);
assert_eq!(n, 0);

let name: Option<String> = None;
let s = name.unwrap_or_else(String::new);
assert_eq!(s, "");

Every one of those fallbacks is just Default::default(). unwrap_or_default reaches for it directly — no literal to pick, no closure to write:

1
2
3
4
5
6
7
8
let count: Option<u32> = None;
assert_eq!(count.unwrap_or_default(), 0);

let name: Option<String> = None;
assert_eq!(name.unwrap_or_default(), "");

let items: Option<Vec<i32>> = None;
assert_eq!(items.unwrap_or_default(), Vec::<i32>::new());

When Some, you get the value untouched; when None, you get T::default().

Where it shines: map lookups

Counting with a HashMap is the classic case — a missing key should read as zero:

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut counts: HashMap<&str, u32> = HashMap::new();
counts.insert("hits", 3);

let hits = counts.get("hits").copied().unwrap_or_default();
let misses = counts.get("misses").copied().unwrap_or_default();
assert_eq!(hits, 3);
assert_eq!(misses, 0);

No .unwrap_or(0) sprinkled at every call site, and if the value type changes, the default follows along automatically.

It works on Result too

Result::unwrap_or_default discards the Err and hands back the default — handy when a parse failure should just mean “nothing”:

1
2
3
4
let good = "42".parse::<i32>().unwrap_or_default();
let bad = "oops".parse::<i32>().unwrap_or_default();
assert_eq!(good, 42);
assert_eq!(bad, 0);

And on your own types

Derive Default and the same trick works for your structs — the fallback stays in one place instead of scattered across the codebase:

1
2
3
4
5
6
7
8
#[derive(Default, Debug, PartialEq)]
struct Config {
    retries: u32,
    verbose: bool,
}

let cfg: Option<Config> = None;
assert_eq!(cfg.unwrap_or_default(), Config { retries: 0, verbose: false });

Reach for unwrap_or_default whenever the fallback is the empty value — let the type decide what empty means.

#225 Jun 2026

225. Option::map_or — Transform-or-Default in One Call, Skip the match

Reaching for a four-line match just to turn an Option into a plain value? map_or does the transform and the fallback in a single call.

The match you keep rewriting

You have an Option, you want a concrete value: apply a function if it’s Some, fall back to a default if it’s None.

1
2
3
4
5
6
7
let name: Option<&str> = Some("ferris");

let len = match name {
    Some(n) => n.len(),
    None => 0,
};
assert_eq!(len, 6);

map_or(default, f) collapses both arms. The first argument is the None fallback, the second is what to do with the value inside Some:

1
2
3
4
5
let name: Option<&str> = Some("ferris");
assert_eq!(name.map_or(0, |n| n.len()), 6);

let missing: Option<&str> = None;
assert_eq!(missing.map_or(0, |n| n.len()), 0);

It beats the common .map(|n| n.len()).unwrap_or(0) too — same result, but no intermediate Option built just to immediately unwrap it.

The catch: the default is eager

map_or takes the default by value, so it’s computed whether or not you need it. With a cheap literal like 0 that’s free. With anything that allocates or does real work, you pay for it even on the Some path:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn expensive_default() -> String {
    // imagine a config read, an allocation, a computation...
    "fallback".to_string()
}

let port: Option<u16> = Some(8080);

// ⚠️ expensive_default() runs even though port is Some
let label = port.map_or(expensive_default(), |p| format!("port {p}"));
assert_eq!(label, "port 8080");

When the fallback isn’t free, switch to map_or_else, which takes a closure and only calls it on None:

1
2
3
4
5
6
7
fn expensive_default() -> String {
    "fallback".to_string()
}

let port: Option<u16> = None;
let label = port.map_or_else(|| expensive_default(), |p| format!("port {p}"));
assert_eq!(label, "fallback");

It works on Result too

Result::map_or follows the same shape — the value goes through f, any Err yields the default:

1
2
3
4
5
let parsed = "42".parse::<i32>();
assert_eq!(parsed.map_or(-1, |n| n * 2), 84);

let bad = "oops".parse::<i32>();
assert_eq!(bad.map_or(-1, |n| n * 2), -1);

Use map_or when the default is a cheap literal, map_or_else when it isn’t, and let the match go.

#224 Jun 2026

224. String::from_utf8_lossy — Returns a Cow, So Valid Bytes Cost Zero

from_utf8_lossy doesn’t always allocate. It hands back a Cow<str> that borrows your bytes when they’re already valid UTF-8 — you only pay for a String when there’s an invalid byte to replace.

The assumption that costs allocations

It’s easy to read this and assume every call builds a fresh String:

1
let text = String::from_utf8_lossy(bytes);

It doesn’t. The return type is Cow<'_, str> — clone-on-write. If bytes is valid UTF-8 (the common case for most files, headers, and protocol fields), you get back a Cow::Borrowed that points straight at your slice. No copy, no heap allocation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::borrow::Cow;

fn read_name(bytes: &[u8]) -> Cow<'_, str> {
    String::from_utf8_lossy(bytes)
}

let valid = b"hello world";
let s = read_name(valid);
assert!(matches!(s, Cow::Borrowed(_))); // borrowed — nothing allocated
assert_eq!(s, "hello world");

You only pay on the rare path

The allocation happens only when there’s an invalid byte to swap for the replacement character U+FFFD. Then — and only then — it builds an owned String:

1
2
3
4
let invalid = &[b'c', b'a', b'f', 0xFF];
let s2 = read_name(invalid);
assert!(matches!(s2, Cow::Owned(_))); // owned — had to fix a bad byte
assert_eq!(s2, "caf\u{fffd}");

So the cost scales with how messy your input is, not with how often you call it.

Don’t undo it with a reflexive .to_string()

The anti-pattern is forcing an allocation right back on:

1
let owned = String::from_utf8_lossy(bytes).to_string(); // ⚠️ always allocates

Keep the Cow for as long as you’re only reading. If a caller genuinely needs ownership, into_owned() allocates on the borrowed path but reuses the buffer on the owned path — no double allocation:

1
2
let owned: String = read_name(b"abc").into_owned();
assert_eq!(owned, "abc");

When you’re decoding bytes you’ll mostly just inspect, let from_utf8_lossy stay a Cow. Valid input — the usual case — flows through without touching the heap.

223. count_ones — Count Set Bits Without a Loop

Need to count how many bits are set in an integer — flags in a bitmask, a population count, a Hamming weight? Don’t write a shift-and-mask loop. Every integer type has .count_ones(), and it usually lowers to a single CPU instruction.

The hand-rolled version is a loop that masks the low bit and shifts:

1
2
3
4
5
6
7
8
fn count_set_bits(mut n: u32) -> u32 {
    let mut count = 0;
    while n != 0 {
        count += n & 1;
        n >>= 1;
    }
    count
}

It works, but it’s a loop you have to get right, and it’s slower than the hardware can do the same job.

Enter count_ones

1
2
let flags: u32 = 0b1011_0010;
assert_eq!(flags.count_ones(), 4);

One call. It’s available on every integer type (u8..u128, i8..i128), and on most targets it compiles straight to a popcnt instruction.

Where it earns its keep

Hamming distance — XOR two values, then count the bits that differ:

1
2
3
let a: u8 = 0b1100_1010;
let b: u8 = 0b1001_1011;
assert_eq!((a ^ b).count_ones(), 3);

Power-of-two test — a power of two has exactly one bit set:

1
2
3
4
5
6
fn is_power_of_two(n: u32) -> bool {
    n.count_ones() == 1
}

assert!(is_power_of_two(64));
assert!(!is_power_of_two(48));

The rest of the family

count_zeros, leading_zeros, trailing_zeros, leading_ones, and trailing_ones round it out — all single-instruction on modern CPUs. leading_zeros is the trick behind a fast integer log2; trailing_zeros gives you the index of the lowest set bit:

1
2
assert_eq!(0b0010_1000u8.trailing_zeros(), 3); // lowest set bit at index 3
assert_eq!(0b0000_1111u8.count_zeros(), 4);

Next time you reach for a bit-counting loop, reach for count_ones instead. Stable since Rust 1.0.

#222 Jun 2026

222. HashSet::intersection / union / difference — Set Math Without the Manual Loops

Need the items two collections have in common, or the ones only in one of them? Don’t write a for loop with .contains() inside. HashSet has set algebra built in.

The hand-rolled intersection is a loop, a temp Vec, and a membership check you have to get right:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::collections::HashSet;

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6].into_iter().collect();

let mut common = Vec::new();
for x in &a {
    if b.contains(x) {
        common.push(*x);
    }
}
common.sort();
assert_eq!(common, vec![3, 4]);

HashSet gives you the four set operations directly. Each returns a lazy iterator that borrows both sets, so nothing is allocated until you collect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::HashSet;

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6].into_iter().collect();

// In both
let mut common: Vec<i32> = a.intersection(&b).copied().collect();
common.sort();
assert_eq!(common, vec![3, 4]);

// In either
let mut all: Vec<i32> = a.union(&b).copied().collect();
all.sort();
assert_eq!(all, vec![1, 2, 3, 4, 5, 6]);

// In a, not in b
let mut only_a: Vec<i32> = a.difference(&b).copied().collect();
only_a.sort();
assert_eq!(only_a, vec![1, 2]);

// In exactly one
let mut either: Vec<i32> = a.symmetric_difference(&b).copied().collect();
either.sort();
assert_eq!(either, vec![1, 2, 5, 6]);

The iterators yield &T, which is why .copied() shows up before collect — drop it if you’d rather collect references. (The .sort() calls are only there to make the asserts deterministic; set iteration order isn’t fixed.)

Just asking a yes/no question?

If you only need to know the relationship, not materialize it, three predicates answer in one call without building anything:

1
2
3
4
5
6
7
8
9
use std::collections::HashSet;

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let small: HashSet<i32> = [3, 4].into_iter().collect();
let far: HashSet<i32> = [9, 10].into_iter().collect();

assert!(small.is_subset(&a));
assert!(a.is_superset(&small));
assert!(a.is_disjoint(&far));

Whenever you catch yourself looping over one collection to test membership in another, reach for these instead — the intent reads straight off the method name.