#241 Jul 2026

241. slice::split_first — Peel the Head Off a Slice, Keep the Tail, No Indexing

slice[0] panics on an empty slice, and &slice[1..] is a second chance to get the bounds wrong. split_first hands you the head and the tail together — or None if there’s nothing there — so the empty case is a pattern, not a panic.

The manual head-and-tail

Reach for the first element and the rest, and you write two indexing operations that both assume the slice is non-empty:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(items: &[&str]) -> String {
    if items.is_empty() {
        return "nothing".to_string();
    }
    let first = items[0];      // panics if you forget the guard above
    let rest = &items[1..];    // and so does this
    format!("{first} plus {} more", rest.len())
}

assert_eq!(describe(&["a", "b", "c"]), "a plus 2 more");

The bounds check is real, but it’s on you to remember the is_empty guard. Drop it and an empty slice panics at runtime.

split_first gives you both, safely

1
2
3
4
5
6
7
8
9
fn describe(items: &[&str]) -> String {
    match items.split_first() {
        Some((first, rest)) => format!("{first} plus {} more", rest.len()),
        None => "nothing".to_string(),
    }
}

assert_eq!(describe(&["a", "b", "c"]), "a plus 2 more");
assert_eq!(describe(&[]), "nothing"); // no panic — the None arm handles it

split_first returns Option<(&T, &[T])>: the first element and a slice of everything after it, or None when the slice is empty. The empty case can’t be forgotten — it’s a variant you have to match.

split_last peels from the other end

1
2
3
4
5
6
let path = ["usr", "local", "bin"];

if let Some((last, parents)) = path.split_last() {
    assert_eq!(*last, "bin");
    assert_eq!(parents, &["usr", "local"]);
}

Same shape, mirrored: the last element plus everything before it.

Where it shines: recursion without index math

Because the tail is just another slice, split_first makes structural recursion clean — the base case is None, and there’s no i + 1 to fumble:

1
2
3
4
5
6
7
8
9
fn sum(slice: &[i32]) -> i32 {
    match slice.split_first() {
        Some((head, tail)) => head + sum(tail),
        None => 0,
    }
}

assert_eq!(sum(&[1, 2, 3, 4]), 10);
assert_eq!(sum(&[]), 0);

Need to mutate as you walk? split_first_mut and split_last_mut return (&mut T, &mut [T]), so you can edit the head and recurse into the tail without a borrow fight. Whenever you catch yourself pairing slice[0] with &slice[1..], this is the method that folds both — and the empty check — into one.

#240 Jul 2026

240. to_le_bytes / from_le_bytes — Serialize Integers Without unsafe or Bit-Shifting

Packing a u32 into four bytes by hand means a stack of >> and as u8 casts — and one wrong shift silently corrupts your data. Every integer type already knows how to lay itself out, in the exact endianness you ask for.

The hand-rolled shift-and-mask

Serializing an integer to bytes the manual way is easy to get subtly wrong:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let n: u32 = 300;

// big-endian by hand: shift, cast, repeat
let bytes = [
    (n >> 24) as u8,
    (n >> 16) as u8,
    (n >> 8) as u8,
    n as u8,
];
assert_eq!(bytes, [0, 0, 1, 44]);

It works, but the shift amounts are magic numbers, the byte order is implicit, and reversing it (parsing bytes back into a u32) means another shift ladder in the opposite direction.

to_*_bytes hands you the array

Every integer type has to_be_bytes, to_le_bytes, and to_ne_bytes, each returning a fixed-size [u8; N]:

1
2
3
4
let n: u32 = 0x12345678;

assert_eq!(n.to_be_bytes(), [0x12, 0x34, 0x56, 0x78]); // big-endian
assert_eq!(n.to_le_bytes(), [0x78, 0x56, 0x34, 0x12]); // little-endian

The endianness is spelled out in the method name, the array length is checked at compile time ([u8; 4] for a u32), and there’s no cast to fumble.

from_*_bytes parses it back

The inverse takes the same fixed-size array and rebuilds the integer:

1
2
3
4
let n: u32 = 300;
let wire = n.to_be_bytes();

assert_eq!(u32::from_be_bytes(wire), 300); // exact round-trip

Reading an integer out of a buffer

The array size is part of the type, so slice a &[u8] and try_into an array before parsing — the length check happens for you:

1
2
3
4
let buf = [0x00, 0x00, 0x01, 0x2c, 0xff]; // 4-byte int + trailing byte

let n = u32::from_be_bytes(buf[..4].try_into().unwrap());
assert_eq!(n, 300);

If the slice is the wrong length, try_into returns an Err instead of reading past the end — no unsafe, no out-of-bounds read.

Use to_be_bytes / from_be_bytes for network and file formats (big-endian is the usual “wire” order), _le_ when a spec demands little-endian, and reach for _ne_ (native) only for in-memory blobs that never leave the machine. Floats have the same methods, and to_bits / from_bits if you want the raw u32/u64 first.

#239 Jul 2026

239. Vec::drain — Remove a Range and Keep What You Pulled Out

truncate throws elements away. split_off allocates a second Vec. When you want to remove a range from a Vec and actually use those elements — and keep the rest — drain hands them to you as an iterator and shifts everything else down for you.

The manual remove-and-collect

Say you want to pull a batch out of the front of a queue and process it:

1
2
3
4
5
6
7
8
9
let mut queue = vec![1, 2, 3, 4, 5];

// take the first three, keep the rest
let mut batch = Vec::new();
for _ in 0..3 {
    batch.push(queue.remove(0)); // each remove is O(n): shifts everything left
}
assert_eq!(batch, vec![1, 2, 3]);
assert_eq!(queue, vec![4, 5]);

Every remove(0) shifts the whole tail down one slot — quadratic for a batch, and the intent is buried in a loop.

drain does it in one pass

1
2
3
4
5
6
let mut queue = vec![1, 2, 3, 4, 5];

let batch: Vec<_> = queue.drain(0..3).collect();

assert_eq!(batch, vec![1, 2, 3]);
assert_eq!(queue, vec![4, 5]);

drain(range) removes that range, yields the removed elements in order, and shifts the remaining tail down once. You own the drained values — collect them, iterate them, or pipe them straight into another call.

Drain the whole thing to reuse the allocation

drain(..) empties the Vec but keeps its capacity, so the buffer is ready to refill without reallocating:

1
2
3
4
5
6
7
8
let mut buf = vec![10, 20, 30];
let cap = buf.capacity();

let sum: i32 = buf.drain(..).sum(); // consume every element by value
assert_eq!(sum, 60);

assert!(buf.is_empty());
assert_eq!(buf.capacity(), cap); // allocation retained — refill it next loop

That’s the move-out-and-reuse trick: unlike into_iter(), which consumes the Vec, drain(..) leaves you an empty-but-allocated Vec to keep using.

The removal happens even if you don’t consume it

Drain is a draining iterator: dropping it removes the range regardless of how many items you pulled. So queue.drain(1..4); on its own still deletes that range — you don’t have to .collect() to make it take effect.

1
2
3
let mut v = vec!['a', 'b', 'c', 'd', 'e'];
v.drain(1..4); // dropped immediately; range is gone
assert_eq!(v, vec!['a', 'e']);

Reach for drain over retain when you’re removing a contiguous range by position (not a predicate), and over split_off when you don’t want a second allocation. If you need conditional removal from anywhere, that’s extract_if; if you just want the values gone, truncate is cheaper.

#238 Jul 2026

238. slice::chunks_exact_mut — Edit a Slice in Fixed-Size Blocks Without Index Math

Processing a buffer N elements at a time usually means a while i + N <= len loop and a pile of buf[i..i+N] slicing. chunks_exact_mut hands you each fixed-size block as a &mut [T] — no index bookkeeping, no off-by-one.

The manual-window loop

1
2
3
4
5
6
7
let mut buf = [10, 20, 30, 40, 50, 60];
let mut i = 0;
while i + 2 <= buf.len() {
    buf[i..i + 2].reverse(); // swap each adjacent pair
    i += 2;
}
assert_eq!(buf, [20, 10, 40, 30, 60, 50]);

It works, but every one of i + 2, <= len, and i += 2 is a place to get the bounds wrong.

chunks_exact_mut yields the blocks for you

1
2
3
4
5
let mut buf = [10, 20, 30, 40, 50, 60];
for pair in buf.chunks_exact_mut(2) {
    pair.reverse();
}
assert_eq!(buf, [20, 10, 40, 30, 60, 50]);

Each pair is a &mut [T; 2]-shaped slice you can mutate in place. The iterator stops once fewer than 2 elements remain, so you never index past the end.

The “exact” part: a leftover tail is skipped, not sliced

Unlike chunks_mut, the last partial block is not yielded — that guarantee is exactly why the block size is reliable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut buf = [1, 2, 3, 4, 5]; // 5 isn't a multiple of 2
let mut it = buf.chunks_exact_mut(2);
for pair in it.by_ref() {
    pair.swap(0, 1);
}
// grab the odd element that didn't fill a block
let tail = it.into_remainder();
assert_eq!(tail, &mut [5][..]);

assert_eq!(buf, [2, 1, 4, 3, 5]); // last 5 left untouched

Reach for the remainder through by_ref() + into_remainder(): iterate the full blocks, then claim whatever fell short. If you drop the loop’s by_ref(), the iterator is moved and into_remainder is unavailable.

Why not just chunks_mut?

chunks_mut(n) also walks a slice in steps of n, but its final chunk can be shorter than n, so any code assuming a fixed width needs a length check every iteration. chunks_exact_mut trades that partial tail for a compile-time-friendly promise that every yielded block is exactly n long — which also lets the optimizer generate tighter code. There’s a read-only chunks_exact for &[T], and as_chunks_mut if you want real &mut [T; N] arrays instead of slices.

#237 Jul 2026

237. Iterator::eq — Compare Two Sequences Without Collecting Them First

Comparing a Vec to an array, or a filtered iterator to an expected result? You don’t need to collect() both sides into the same type first.

The usual reflex is to force both sequences into matching containers so == works:

1
2
3
4
5
let v = vec![1, 2, 3];
let a = [1, 2, 3];

// Allocates a throwaway Vec just to compare.
assert_eq!(v, a.to_vec());

Iterator::eq compares element-by-element straight off the iterators. No allocation, and the two sides don’t even have to be the same container type:

1
2
3
4
let v = vec![1, 2, 3];
let a = [1, 2, 3];

assert!(v.iter().eq(a.iter()));

It short-circuits, so a length or value mismatch stops early instead of walking both sequences to the end:

1
assert!([1, 2, 3].iter().ne([1, 2].iter()));

The real payoff is comparing a lazy chain to an expected sequence without materializing it:

1
2
let evens = (1..=6).filter(|n| n % 2 == 0);
assert!(evens.eq([2, 4, 6]));

And the same family does lexicographic ordering, again with no intermediate Vec:

1
2
3
4
use std::cmp::Ordering;

assert_eq!([1, 2, 3].iter().cmp([1, 2, 4].iter()), Ordering::Less);
assert!([1, 2].iter().lt([1, 3].iter()));

So the toolkit is eq, ne, cmp, lt, le, gt, ge — full equality and ordering over any two iterators, no collecting required.

236. Result::flatten — Collapse a Result<Result<T, E>, E> in One Call

Ended up with a Result<Result<T, E>, E> and reached for a nested match to peel it? When both layers share the same error type, Result::flatten (stable since 1.89) does it in one call.

How you get a doubly-wrapped Result

It usually sneaks in when you map a fallible operation over something that’s already a Result:

1
2
3
4
5
6
7
8
fn first_word(line: Result<&str, String>) -> Result<Result<u32, String>, String> {
    line.map(|s| {
        s.split_whitespace()
            .next()
            .ok_or_else(|| "empty line".to_string())
            .and_then(|w| w.parse::<u32>().map_err(|e| e.to_string()))
    })
}

The outer Result is “did we have a line?”, the inner one is “did it parse?”. Same String error on both — but the type is now Result<Result<u32, String>, String>, which no caller wants to touch.

The manual peel

1
2
3
4
5
6
7
8
let nested: Result<Result<u32, String>, String> = Ok(Ok(42));

let flat: Result<u32, String> = match nested {
    Ok(inner) => inner,   // inner is itself a Result
    Err(e) => Err(e),
};

assert_eq!(flat, Ok(42));

Correct, but it’s boilerplate that hides the intent.

flatten does exactly this

1
2
3
4
5
6
7
let ok:    Result<Result<u32, String>, String> = Ok(Ok(42));
let inner: Result<Result<u32, String>, String> = Ok(Err("bad".into()));
let outer: Result<Result<u32, String>, String> = Err("io".into());

assert_eq!(ok.flatten(),    Ok(42));
assert_eq!(inner.flatten(), Err("bad".to_string()));
assert_eq!(outer.flatten(), Err("io".to_string()));

Ok(Ok(x)) becomes Ok(x); either Err — inner or outer — passes straight through. Option has the same method, Option::flatten, for Option<Option<T>>.

The catch: error types must match

flatten is Result<Result<T, E>, E>Result<T, E> — one E. If the two layers carry different error types, flatten can’t merge them. Reach for and_then instead, which lets ?-style conversion do the work:

1
2
3
4
5
let nested: Result<Result<u32, String>, String> = Ok(Ok(7));

// same as .flatten() here, but and_then can also adapt the inner error
let flat = nested.and_then(|inner| inner);
assert_eq!(flat, Ok(7));

When the errors already line up, flatten is the clearest way to say “unwrap one layer of nesting.”

#235 Jul 2026

235. Iterator::rposition — Find the Last Match Without Reversing-and-Subtracting

Need the index of the last element that matches? The reflex is iter().rev().position(...) then len - 1 - i — and that arithmetic is a classic off-by-one. rposition searches from the back and hands you the real forward index.

The reversed-index trap

1
2
3
4
5
6
7
let bytes = [b'a', b'/', b'b', b'/', b'c'];

// "index of the last slash" — the fragile way
let from_end = bytes.iter().rev().position(|&b| b == b'/').unwrap();
let idx = bytes.len() - 1 - from_end;

assert_eq!(idx, 3);

position on a reversed iterator counts from the end, so you have to flip it back with len - 1 - i. Get the - 1 wrong and you’re off by one.

rposition does the flip for you

1
2
3
4
5
let bytes = [b'a', b'/', b'b', b'/', b'c'];

let idx = bytes.iter().rposition(|&b| b == b'/');

assert_eq!(idx, Some(3)); // real index, counted from the front

rposition walks from the back but returns the index in the original, forward order — no arithmetic, and None when nothing matches instead of a panic on the empty case.

It short-circuits from the right

Just as position stops at the first match from the front, rposition stops at the first match from the back — so it only scans the tail it needs:

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

// last even number
let idx = nums.iter().rposition(|&n| n % 2 == 0);
assert_eq!(idx, Some(5)); // stopped immediately at 6

The requirement

rposition needs a DoubleEndedIterator (so it can walk backwards) that is also an ExactSizeIterator (so it knows the length to report the front index). Slices, Vec, and arrays give you both. A lazy adapter like filter isn’t ExactSizeIterator, so index a slice or collect first.

For string byte or substring searches, str::rfind is the more direct tool — but for an arbitrary predicate over any exact-size sequence, rposition is the one to reach for.

#234 Jul 2026

234. core::range::Range — A Range That's Copy, So You Can Store and Reuse It

The classic 0..3 isn’t Copy — it is the iterator, so it gets consumed and can’t live in a Copy struct. Rust 1.96 stabilises core::range::Range, which is Copy and iterates through IntoIterator instead.

Why the old Range fights you

std::ops::Range implements Iterator directly. That’s convenient in a for loop, but it means the range is mutable iterator state, so it can’t be Copy:

1
2
3
let r = 0..3;
let total: i32 = r.sum();   // moves r
// let n = r.count();       // error: use of moved value: r

It also can’t sit inside a Copy type, which is annoying when you want a lightweight span:

1
2
#[derive(Clone, Copy)]        // error: the trait Copy is not
struct Span(std::ops::Range<usize>);  // implemented for Range<usize>

The new core::range::Range is Copy

Stabilised in 1.96, core::range::Range implements IntoIterator rather than Iterator, which frees it to be Copy. Convert a literal range into it with .into():

1
2
3
4
5
6
7
use core::range::Range;

let r: Range<usize> = (0..3).into();
let total: usize = r.into_iter().sum();   // r is Copy, not moved
let count = r.into_iter().count();        // still usable
assert_eq!(total, 3);
assert_eq!(count, 3);

Now it fits in a Copy struct

Because it’s Copy, you can store a range as a cheap, copyable span and still index slices with it directly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use core::range::Range;

#[derive(Clone, Copy)]
struct Span(Range<usize>);

impl Span {
    fn of(self, s: &str) -> &str { &s[self.0] }
}

let span = Span((2..5).into());
let a = span;                 // copies, no move
assert_eq!(a.of("hello world"), "llo");
assert_eq!(span.of("hello world"), "llo");   // span still valid

No more splitting a range into separate start / end fields just to keep a struct Copy.

Heads up

0..3 syntax still produces the legacy std::ops::Range for now — the new types come in via .into() or by naming core::range::Range. A future edition will switch the syntax over. For public APIs that should accept either, take impl RangeBounds<usize>.

Stabilised in Rust 1.96 (May 2026).

233. str::split_terminator — Split Without the Trailing Empty String

"a.b.c.".split('.') hands you a phantom empty string at the end. split_terminator reads the separator as ending each piece, not delimiting it — so the trailing one just vanishes.

Split counts the region after the final separator, even when it’s empty. Feed it text that ends with the separator and you get a ghost element:

1
2
let parts: Vec<&str> = "a.b.c.".split('.').collect();
assert_eq!(parts, ["a", "b", "c", ""]);

That trailing "" is a landmine downstream: an empty record, a blank line, a zero-length field you didn’t ask for.

split_terminator treats the separator as terminating each piece, so a trailing separator produces no empty tail:

1
2
let parts: Vec<&str> = "a.b.c.".split_terminator('.').collect();
assert_eq!(parts, ["a", "b", "c"]);

It only drops the empty piece caused by a trailing separator — interior empties are still real data and still show up:

1
2
let parts: Vec<&str> = "a..b.".split_terminator('.').collect();
assert_eq!(parts, ["a", "", "b"]);

And with no trailing separator it behaves exactly like split:

1
2
let parts: Vec<&str> = "a.b.c".split_terminator('.').collect();
assert_eq!(parts, ["a", "b", "c"]);

Reach for it when parsing records that end with their delimiter — newline-terminated logs, ;-terminated statements, trailing-comma lists — and skip the .filter(|s| !s.is_empty()) cleanup.

#232 Jun 2026

232. Iterator::partition — Split Into Two Collections in One Pass

Splitting items into “matches” and “the rest” with two .filter() calls walks the list twice. partition does it in a single pass.

The two-filter version reads fine but iterates the whole thing twice, once per predicate:

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

let even: Vec<i32> = nums.iter().copied().filter(|n| n % 2 == 0).collect();
let odd:  Vec<i32> = nums.iter().copied().filter(|n| n % 2 != 0).collect();

The manual loop is one pass but needs two mutable Vecs and the bookkeeping to feed them. partition is the one-pass version with none of the noise — true items go left, false items go right:

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

let (even, odd): (Vec<i32>, Vec<i32>) = nums.iter().partition(|&&n| n % 2 == 0);
assert_eq!(even, [2, 4, 6, 8]);
assert_eq!(odd,  [1, 3, 5, 7]);

The collection types come from the annotation, and they don’t have to be Vec or even match each other — any pair that implements FromIterator works:

1
2
3
4
5
6
let words = ["hi", "hello", "yo", "howdy"];

let (long, short): (Vec<&str>, Vec<&str>) =
    words.into_iter().partition(|s| s.len() > 3);
assert_eq!(long,  ["hello", "howdy"]);
assert_eq!(short, ["hi", "yo"]);

Whenever you catch yourself filtering the same iterator twice with opposite conditions, that’s a partition.