Collections

#143 May 2026

143. Vec::dedup_by_key — Collapse Consecutive Duplicates by a Derived Key

Vec::dedup() only collapses runs that are exactly equal. When you care about a derived attribute — the minute on a timestamp, the domain in an email, the first letter of a word — reach for dedup_by_key.

The plain dedup() is strict: two adjacent elements are only merged if == says so.

1
2
3
let mut nums = vec![1, 1, 2, 3, 3, 3, 2, 5];
nums.dedup();
assert_eq!(nums, vec![1, 2, 3, 2, 5]);

But often “duplicate” really means “shares some property with its neighbour.” dedup_by_key takes a closure that maps each element to a key, then keeps the first of every consecutive run whose keys match:

1
2
3
let mut nums = vec![1, 3, 5, 2, 4, 7, 6, 8];
nums.dedup_by_key(|n| *n % 2);
assert_eq!(nums, vec![1, 2, 7, 6]);

1, 3, 5 all have key 1 → keep 1. Then 2, 4 have key 0 → keep 2. Then 7 has key 1 → keep it. Then 6, 8 have key 0 → keep 6.

The practical case: log lines already in time order, and you want one representative per minute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let mut lines = vec![
    "12:01 GET /a".to_string(),
    "12:01 GET /b".to_string(),
    "12:01 POST /c".to_string(),
    "12:02 GET /d".to_string(),
    "12:02 GET /e".to_string(),
    "12:03 PATCH /f".to_string(),
];

lines.dedup_by_key(|line| line[..5].to_string());

assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "12:01 GET /a");
assert_eq!(lines[1], "12:02 GET /d");
assert_eq!(lines[2], "12:03 PATCH /f");

Two things worth remembering. First, it only looks at adjacent pairs — if you need full uniqueness, pair it with sort_by_key first. Second, if your equivalence isn’t expressible as a key (e.g. “values within 0.1 of each other”), there’s a sibling dedup_by that takes |a, b| -> bool directly. All three are in-place, allocation-free, and run in linear time.

141. BinaryHeap::into_sorted_vec — Heapsort in One Call

You stuffed everything into a BinaryHeap to keep “biggest first” cheap, but at the end of the day you want a sorted Vec to hand to the next stage. The pop-loop you almost wrote is built into the type — into_sorted_vec consumes the heap and gives you the ascending-order Vec for free.

The pop-loop

The naive shape: drain the heap with pop and push into a fresh Vec. Pops come out largest-first, so to get ascending order you have to either reverse at the end or push to the front — both add steps for no reason.

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

let heap = BinaryHeap::from([3, 1, 4, 1, 5, 9, 2, 6, 5]);

let mut out = Vec::with_capacity(heap.len());
let mut heap = heap;
while let Some(x) = heap.pop() {
    out.push(x);
}
out.reverse(); // pops came out descending — flip them

assert_eq!(out, [1, 1, 2, 3, 4, 5, 5, 6, 9]);

Five lines and a temporary out for what is, in the end, “sort this thing.” The heap is already a heap — you’ve paid for the structure, now you’re throwing it away.

The one-liner

BinaryHeap::into_sorted_vec(self) -> Vec<T> does exactly the drain-and-sort the heap was built for, in O(n log n), and reuses the heap’s allocation as the output Vec. No reverse(), no spare buffer.

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

let heap = BinaryHeap::from([3, 1, 4, 1, 5, 9, 2, 6, 5]);
let sorted = heap.into_sorted_vec();

assert_eq!(sorted, [1, 1, 2, 3, 4, 5, 5, 6, 9]);

Ascending order, because that’s almost always what the next consumer wants. BinaryHeap is a max-heap, so internally into_sorted_vec repeatedly sifts the max to the end of the backing buffer — the same in-place heapsort you’d write by hand.

Top-k without sorting the whole input

Where this really pays off: “I want the largest k of n items.” Push everything into the heap with Reverse to make it a min-heap-of-size-k, then call into_sorted_vec once at the end:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::cmp::Reverse;
use std::collections::BinaryHeap;

fn top_k(items: impl IntoIterator<Item = i32>, k: usize) -> Vec<i32> {
    let mut heap: BinaryHeap<Reverse<i32>> = BinaryHeap::with_capacity(k);
    for x in items {
        if heap.len() < k {
            heap.push(Reverse(x));
        } else if let Some(mut min) = heap.peek_mut() {
            if x > min.0 { *min = Reverse(x); }
        }
    }
    heap.into_sorted_vec()
        .into_iter()
        .map(|Reverse(x)| x)
        .collect()
}

assert_eq!(top_k([7, 3, 9, 1, 8, 2, 6], 3), [9, 8, 7]);

into_sorted_vec returns the Reverse-wrapped items in ascending Reverse order, which is descending by inner value — strip the wrapper with map and the largest of the top-k comes out first, exactly the order a “top” list wants.

When to reach for it

Any time the loop you’re about to write is “pop until empty, collect into Vec.” into_sorted_vec is the same algorithm — heapsort — with one fewer allocation and one fewer reverse. The heap was already half of a sort; let it finish the job.

#139 May 2026

139. HashMap::extract_if — Drain Matching Entries Without Losing Them

HashMap::retain discards what it removes. When you actually want those values — to log them, ship them, or move them elsewhere — you used to clone the keys and double-walk the map. extract_if, stable since Rust 1.88, hands them back as an iterator.

retain throws the babies out with the bathwater

retain is the natural “filter a map in place” call, but its return type is (). The entries it kicks out vanish:

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

let mut sessions: HashMap<&str, u32> = HashMap::from([
    ("alice", 12),
    ("bob",    0),
    ("carol",  3),
    ("dan",    0),
]);

sessions.retain(|_user, hits| *hits > 0);

assert_eq!(sessions.len(), 2);
// Who got dropped? Gone. We can't tell anyone.

If you wanted to log the expired sessions, notify their owners, or forward them to another map, retain isn’t enough. The textbook workaround is collect-then-remove:

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

let mut sessions: HashMap<String, u32> = HashMap::from([
    ("alice".into(), 12),
    ("bob".into(),    0),
    ("carol".into(),  3),
    ("dan".into(),    0),
]);

let to_evict: Vec<String> = sessions
    .iter()
    .filter(|(_, hits)| **hits == 0)
    .map(|(k, _)| k.clone())          // forced clone to break the self-borrow
    .collect();

let mut evicted = Vec::new();
for k in to_evict {
    if let Some(v) = sessions.remove(&k) {
        evicted.push((k, v));
    }
}

assert_eq!(evicted.len(), 2);

Two passes, a throwaway Vec<K>, and a hard K: Clone requirement just so the borrow checker stops yelling.

extract_if is one pass and gives the values back

HashMap::extract_if(pred) returns an iterator that yields (K, V) for every entry whose predicate fires true, removing them from the map as it goes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::collections::HashMap;

let mut sessions: HashMap<&str, u32> = HashMap::from([
    ("alice", 12),
    ("bob",    0),
    ("carol",  3),
    ("dan",    0),
]);

let evicted: HashMap<&str, u32> =
    sessions.extract_if(|_user, hits| *hits == 0).collect();

assert_eq!(evicted.len(), 2);
assert_eq!(sessions.len(), 2);
assert!(sessions.contains_key("alice"));
assert!(sessions.contains_key("carol"));

No clone, no temporary Vec, no second walk. The removed entries land in whatever collection you choose — another HashMap, a Vec<(K, V)>, a channel, a log line. HashMap order isn’t guaranteed, but neither was it before.

The predicate gets &mut V, so it can edit survivors

The closure signature is FnMut(&K, &mut V) -> bool. Return true to extract, false to keep — and because you’ve got &mut V in hand either way, you can update entries you choose to leave behind:

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

let mut scores: HashMap<&str, i32> = HashMap::from([
    ("alice", 85),
    ("bob",   42),
    ("carol", 91),
    ("dan",   30),
]);

let failing: HashMap<&str, i32> = scores
    .extract_if(|_name, score| {
        if *score < 50 {
            true             // pull this one out
        } else {
            *score += 5;     // bump the survivors
            false            // keep them
        }
    })
    .collect();

assert_eq!(failing.len(), 2);
assert_eq!(scores[&"alice"], 90);
assert_eq!(scores[&"carol"], 96);

One traversal, three jobs: filter, remove-and-collect, and patch in place.

Drop the iterator early and the rest stay

The iterator only removes entries it has visited. If you stop iterating — break, take(n), drop the iterator — anything not yet inspected stays in the map untouched:

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

let mut nums: HashMap<i32, i32> = (0..10).map(|i| (i, i)).collect();

// Pull at most two even entries out, then stop.
let pulled: Vec<(i32, i32)> = nums
    .extract_if(|_, v| *v % 2 == 0)
    .take(2)
    .collect();

assert_eq!(pulled.len(), 2);
// 10 - 2 = 8 left, regardless of which evens were taken.
assert_eq!(nums.len(), 8);

When to reach for it

Whenever you’d otherwise write the clone-keys-and-double-pass shuffle: expiring sessions, draining tasks that hit a deadline, partitioning a cache by tenant, moving “done” items into an archive map. HashSet::extract_if is the same idea for sets — predicate is FnMut(&T) -> bool. The mental rule: reach for retain when the discarded entries are truly garbage, and extract_if when they still have a job to do.

#137 May 2026

137. Vec::retain_mut — Filter and Edit In Place, In One Pass

retain decides who stays, but its closure only sees &T — so when you also want to tweak the survivors, you end up making two passes. retain_mut collapses that into one.

The Problem

Imagine a Vec<Job> where each surviving job needs its retry counter bumped on the way through. With plain retain, the closure gets a shared reference, so the mutation has to happen separately:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Job { id: u32, retries: u32, done: bool }

let mut jobs = vec![
    Job { id: 1, retries: 0, done: false },
    Job { id: 2, retries: 0, done: true  },
    Job { id: 3, retries: 0, done: false },
];

// Pass 1: bump the survivors.
for j in jobs.iter_mut().filter(|j| !j.done) {
    j.retries += 1;
}
// Pass 2: drop the finished ones.
jobs.retain(|j| !j.done);

Two passes, two intent-leaks, and the predicate is duplicated. Refactor one and forget to refactor the other — welcome to a subtle bug.

retain_mut: One Pass, Mutable Access

Vec::retain_mut gives the closure a &mut T. Edit the element and return whether to keep it — same call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct Job { id: u32, retries: u32, done: bool }

let mut jobs = vec![
    Job { id: 1, retries: 0, done: false },
    Job { id: 2, retries: 0, done: true  },
    Job { id: 3, retries: 0, done: false },
];

jobs.retain_mut(|j| {
    if j.done { return false; }
    j.retries += 1;
    true
});

assert_eq!(jobs.len(), 2);
assert_eq!(jobs[0].id, 1);
assert_eq!(jobs[0].retries, 1);
assert_eq!(jobs[1].id, 3);
assert_eq!(jobs[1].retries, 1);

One scan, no duplicated predicate, and the order of the kept elements is preserved.

The Sneaky Trick: Mutate-Then-Check

The mutation happens before the closure returns, so a retain_mut can also normalize values and then filter on the normalized form:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut words = vec![
    String::from("  hello "),
    String::from("   "),
    String::from(" Rust"),
];

words.retain_mut(|s| {
    *s = s.trim().to_string();
    !s.is_empty()
});

assert_eq!(words, vec!["hello", "Rust"]);

Trim everything, then drop the blanks — without ever allocating a second Vec.

When to Reach for It

Use retain_mut whenever the keep/drop decision and an in-place edit travel together. Same goes for VecDeque::retain_mut and LinkedList::retain_mut — same shape, same payoff. If the closure is purely read-only, stick with retain and keep the intent narrow.

#129 May 2026

129. BTreeMap::extract_if — Range-Scoped Filter-and-Remove in One Pass

Vec::extract_if was great. The BTreeMap version, stable since Rust 1.91, adds a trick the slice variant can’t pull off — a range bound that scopes the scan to part of the keyspace.

The collect-keys-then-remove dance

You want to remove every entry matching a predicate from a BTreeMap, and keep the removed entries. The borrow checker won’t let you mutate the map while you’re iterating it, so the textbook workaround is a two-pass clone-the-keys shuffle:

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

let mut events: BTreeMap<u64, String> = BTreeMap::from([
    (1, "boot".into()),
    (2, "login".into()),
    (3, "click".into()),
    (4, "logout".into()),
]);

let cutoff = 3;

let to_remove: Vec<u64> = events
    .iter()
    .filter(|(k, _)| **k < cutoff)
    .map(|(k, _)| *k)            // requires K: Copy or Clone
    .collect();

let mut expired = Vec::new();
for k in to_remove {
    if let Some(v) = events.remove(&k) {
        expired.push((k, v));
    }
}

assert_eq!(expired.len(), 2);

It works, but the costs add up. You walk the map twice, you require K: Clone (or Copy), and you allocate a throwaway Vec<K> just to break the self-borrow.

extract_if is one pass — and it takes a range

BTreeMap::extract_if(range, pred) returns an iterator that visits keys in ascending order inside the given range, and yields (K, V) for every entry whose predicate returns true. The map’s structure is updated as the iterator advances:

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

let mut events: BTreeMap<u64, &str> = BTreeMap::from([
    (1, "boot"),
    (2, "login"),
    (3, "click"),
    (4, "logout"),
    (5, "shutdown"),
]);

let expired: BTreeMap<u64, &str> =
    events.extract_if(..3, |_k, _v| true).collect();

assert_eq!(expired.into_iter().collect::<Vec<_>>(), [(1, "boot"), (2, "login")]);
assert_eq!(events.keys().copied().collect::<Vec<_>>(), [3, 4, 5]);

No Clone bound. No second pass. No temp Vec<K>. The range ..3 does double duty as a prune — keys outside it aren’t even visited, which matters when the map is large and the range is small.

The closure can mutate survivors too

The predicate signature is FnMut(&K, &mut V) -> bool. Returning true removes and yields; returning false keeps the entry in the map — but you’ve still got &mut V, so you can edit it on the way past:

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

let mut scores: BTreeMap<&str, i32> = BTreeMap::from([
    ("alice", 85),
    ("bob",   42),
    ("carol", 91),
    ("dan",   30),
]);

let failing: Vec<(&str, i32)> = scores
    .extract_if(.., |_k, v| {
        if *v < 50 {
            true            // pull this one out
        } else {
            *v += 5;        // bump the survivors by 5
            false           // keep it
        }
    })
    .collect();

assert_eq!(failing, [("bob", 42), ("dan", 30)]);
assert_eq!(scores[&"alice"], 90);
assert_eq!(scores[&"carol"], 96);

One traversal, three behaviors at once: filter, remove, and update.

Constraints

The range is RangeBounds<K>, so anything from .. to start..=end works. extract_if panics if start > end or if the bounds collapse to an empty exclusive range. BTreeSet::extract_if is the same idea minus the value — predicate signature is FnMut(&T) -> bool.

When to reach for it

Whenever the operation is “walk a sorted map, peel off the entries matching a condition, optionally bounded to a key range”. Expiring old timestamped events. Draining tasks below a priority watermark. Splitting a map at a tenant boundary into “keep” and “ship”. The range bound is the part the Vec::extract_if (bite 43) can’t give you — and on a large map, scoping the scan is the whole point.

#126 May 2026

126. Vec::split_off — Cut a Vec in Two and Keep Both Halves

You want the tail of a Vec as its own owned collection — the head stays put, the tail walks away. Cloning a slice works for Clone types, but breaks the moment your elements aren’t cloneable. Vec::split_off doesn’t care.

The clone-and-truncate dance

The textbook split: copy the tail with to_vec(), then truncate the original.

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

let tail: Vec<i32> = all[3..].to_vec();
all.truncate(3);

assert_eq!(all,  vec![1, 2, 3]);
assert_eq!(tail, vec![4, 5, 6]);

It works, but it clones every element of the tail. Fine for i32, wasteful for String, and a hard error for any type that isn’t Clone.

split_off moves, doesn’t clone

Vec::split_off(at) consumes the elements at at.. out of the original Vec and returns them as a new Vec. The elements are moved, not copied — so it works for any T, Clone or not:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut tasks: Vec<Box<dyn FnOnce()>> = vec![
    Box::new(|| println!("a")),
    Box::new(|| println!("b")),
    Box::new(|| println!("c")),
    Box::new(|| println!("d")),
];

// Box<dyn FnOnce()> isn't Clone — `tasks[2..].to_vec()` won't compile.
let later = tasks.split_off(2);

assert_eq!(tasks.len(), 2);
assert_eq!(later.len(), 2);

The original keeps [0..at), the returned Vec gets [at..len), and not a single element is duplicated. at == 0 gives you the whole thing in the new Vec (the original ends up empty); at == len gives you an empty new Vec. Anything past the length panics.

A real shape: page-by-page draining

split_off shines when you want to peel a batch off the front of a queue and hand it to a worker, keeping the rest for next time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn next_batch<T>(queue: &mut Vec<T>, size: usize) -> Vec<T> {
    let take = size.min(queue.len());
    let rest = queue.split_off(take);
    // queue now holds the first `take` items — that's our batch.
    // `rest` holds everything else — put it back as the new queue.
    let batch = std::mem::replace(queue, rest);
    batch
}

let mut q = vec!["a", "b", "c", "d", "e"];
let first  = next_batch(&mut q, 2);
let second = next_batch(&mut q, 2);

assert_eq!(first,  vec!["a", "b"]);
assert_eq!(second, vec!["c", "d"]);
assert_eq!(q,      vec!["e"]);

No clone, no temporary Vec, no fighting the borrow checker over slice ranges. Two memcpys and you’re done.

When to reach for it

Use split_off whenever you need both halves of a Vec as owned collections — batching, chunked processing, splitting state between threads. If you only want to iterate the tail and throw it away, drain(at..) is better; if you want to keep it, split_off is the move.

#123 May 2026

123. BTreeMap::pop_first — A Sorted Map That Doubles as a Priority Queue

BinaryHeap only goes one way — biggest first. When you want to pull the smallest or the largest from the same collection, reach for BTreeMap and let pop_first / pop_last do the work.

The classic shape: a queue of jobs keyed by priority where you sometimes need the most-urgent job and sometimes the least-urgent one. With BinaryHeap you’d pick a direction and stick with it (or wrap things in Reverse to flip it). With BTreeMap you get both ends for free, because the keys are already sorted:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::collections::BTreeMap;

let mut jobs: BTreeMap<u32, &str> = BTreeMap::new();
jobs.insert(5, "rebuild index");
jobs.insert(1, "send heartbeat");
jobs.insert(9, "page oncall");
jobs.insert(3, "rotate logs");

// Smallest key first — drain by priority.
assert_eq!(jobs.pop_first(), Some((1, "send heartbeat")));
assert_eq!(jobs.pop_first(), Some((3, "rotate logs")));

// Or grab the most urgent from the other end.
assert_eq!(jobs.pop_last(), Some((9, "page oncall")));

// Empty? You get None — same shape as Vec::pop.
let mut empty: BTreeMap<u32, &str> = BTreeMap::new();
assert_eq!(empty.pop_first(), None);

Both methods return Option<(K, V)> and remove the entry from the map. No second lookup, no .remove(key) follow-up after .first_key_value().

Where this really earns its keep is the “drain-in-order” loop — the kind of thing you’d otherwise write with a heap plus a sidecar map:

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

let mut tasks: BTreeMap<u32, String> = BTreeMap::new();
tasks.insert(20, "compact".into());
tasks.insert(10, "vacuum".into());
tasks.insert(30, "snapshot".into());

let mut order = Vec::new();
while let Some((priority, name)) = tasks.pop_first() {
    order.push((priority, name));
}

assert_eq!(
    order,
    vec![
        (10, "vacuum".into()),
        (20, "compact".into()),
        (30, "snapshot".into()),
    ],
);

Same loop, swap pop_first for pop_last and you drain in reverse order — no Reverse wrapper, no second collection.

BTreeSet got the same pair (pop_first / pop_last) at the same time, so a sorted set behaves like a deque you can pop from either end:

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

let mut ids: BTreeSet<u32> = BTreeSet::from([7, 2, 9, 4]);
assert_eq!(ids.pop_first(), Some(2));
assert_eq!(ids.pop_last(),  Some(9));
assert_eq!(ids.len(), 2);

A few things worth knowing. BTreeMap insertion is O(log n) — heavier than a BinaryHeap push, which amortises to O(1). If you genuinely only ever pop from one side and throughput matters, a heap still wins. The moment you need ordered iteration, range queries, or popping from both ends, BTreeMap is the better fit and pop_first / pop_last make that fit feel native.

Stable since Rust 1.66 — and one of those methods that quietly replaces a fistful of match arms once you remember it exists.

#115 May 2026

115. Vec::resize_with — Grow a Vec With a Closure, Not a Clone

Vec::resize makes every new slot a clone of the same value. When you need fresh values per slot — counters, allocations, defaults — resize_with calls a closure for each new element instead.

Vec::resize(n, value) is fine when the filler is cheap and identical, but it has two annoyances. It needs T: Clone, and every new slot is the same clone. So this doesn’t work the way you want:

1
2
3
4
5
// One Vec shared across every slot — mutating slot 0 mutates them all.
let mut grid: Vec<Vec<u8>> = Vec::new();
grid.resize(3, Vec::new());
grid[0].push(42);
assert_eq!(grid[1], vec![]); // fine — Vec::new() clones to a new empty Vec

That one happens to be safe because Vec::clone actually allocates. But the moment your T is Rc<RefCell<…>>, every slot points at the same cell. And if T isn’t Clone at all, you can’t call resize in the first place.

resize_with takes a closure and calls it once per new slot:

1
2
3
4
5
6
7
let mut counter = 0;
let mut v = vec![10, 20];
v.resize_with(5, || {
    counter += 1;
    counter
});
assert_eq!(v, vec![10, 20, 1, 2, 3]);

The closure can capture mutable state, so each call is fresh. Generating IDs, pulling from an RNG, allocating independent buffers — all easy:

1
2
3
4
5
6
7
8
9
let mut next_id = 100;
let mut buffers: Vec<(usize, Vec<u8>)> = Vec::new();
buffers.resize_with(3, || {
    let id = next_id;
    next_id += 1;
    (id, Vec::with_capacity(1024))
});
assert_eq!(buffers[0].0, 100);
assert_eq!(buffers[2].0, 102);

For non-Clone types, Default::default is the usual filler:

1
2
3
4
5
6
7
8
9
#[derive(Default, Debug, PartialEq)]
struct Slot {
    open: bool,
    payload: Vec<u8>,
}

let mut slots: Vec<Slot> = Vec::new();
slots.resize_with(2, Default::default);
assert_eq!(slots, vec![Slot::default(), Slot::default()]);

Shrinking still works, and the closure is never called when the new length is smaller:

1
2
3
let mut v = vec![1, 2, 3, 4, 5];
v.resize_with(2, || unreachable!());
assert_eq!(v, vec![1, 2]);

Reach for resize_with whenever the filler isn’t a single static value — and especially when T doesn’t (or shouldn’t) implement Clone.

109. BinaryHeap::peek_mut — Edit the Top Without Pop-and-Push

Updating the largest element in a BinaryHeap shouldn’t take two heap operations. peek_mut lets you mutate it in place and re-heapifies on drop — one sift instead of two.

The pop-and-push dance

Say you keep tasks in a max-heap by priority and you need to bump the top one down. The naive recipe is to pop, change it, and push it back:

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

let mut heap = BinaryHeap::from([3, 1, 5, 2, 4]);

// Pull the max out, edit it, push it back.
let top = heap.pop().unwrap();
heap.push(top - 10);

assert_eq!(heap.peek(), Some(&4));

That’s two O(log n) heap operations — a sift-down for pop, a sift-up for push — plus an awkward two-step where the heap briefly forgets it ever had a max.

peek_mut does it in one shot

peek_mut returns a PeekMut guard that derefs to the top element. You mutate it in place, and the heap fixes itself with a single sift-down when the guard is dropped:

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

let mut heap = BinaryHeap::from([3, 1, 5, 2, 4]);

if let Some(mut top) = heap.peek_mut() {
    *top -= 10; // 5 becomes -5
}
// PeekMut dropped here — heap re-heapifies once.

assert_eq!(heap.peek(), Some(&4));

One traversal, no temporary owned value, and the heap invariant is restored before the next line of code runs.

Conditionally pop without a re-peek

PeekMut also has an associated pop function. Look at the top, decide whether to keep or remove it, all without peeking twice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::collections::{BinaryHeap, binary_heap::PeekMut};

let mut heap = BinaryHeap::from([10, 7, 4, 1]);

if let Some(top) = heap.peek_mut() {
    if *top > 5 {
        // Pop only when the top passes a check.
        PeekMut::pop(top);
    }
}

assert_eq!(heap.peek(), Some(&7));

Same shape as Vec::pop_if, but for the heap’s max — pull the top out only when it actually meets your condition.

When to reach for it

Any time you’d write pop().push() to edit the max, peek_mut is the better tool: one heap fixup instead of two, and the borrow stays inside the heap so you don’t shuffle ownership for nothing. Great fit for priority queues that adjust the top entry — schedulers, event loops, top-k trackers.

#107 Apr 2026

107. Vec::splice — Replace a Range and Keep the Old Values

Need to swap a chunk of a Vec for a different number of items? Most people reach for drain plus extend and a bunch of bookkeeping. Vec::splice does the whole thing in one call — and even hands back what it removed.

The clunky way

You want to replace v[1..4] with two different values. Without splice, you end up juggling indices:

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

// Remove the middle, save the tail, then stitch it back together.
let tail: Vec<_> = v.drain(4..).collect();
v.truncate(1);
v.extend([10, 20]);
v.extend(tail);

assert_eq!(v, [1, 10, 20, 5]);

Three steps, one temporary Vec, and you didn’t even capture what was removed. Easy to off‑by‑one, easy to get wrong.

Enter splice

splice takes a range and an iterator and swaps them in place — the replacement can be any length:

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

v.splice(1..4, [10, 20]);

assert_eq!(v, [1, 10, 20, 5]);

One call. No tail buffer. Works whether the replacement is shorter, longer, or the same size as the range.

It returns the removed items too

splice is a draining iterator — collect it and you get the elements that were taken out:

1
2
3
4
5
6
let mut v = vec![10, 20, 30, 40, 50];

let removed: Vec<_> = v.splice(1..4, [99]).collect();

assert_eq!(removed, [20, 30, 40]);
assert_eq!(v, [10, 99, 50]);

Perfect for “swap this section, but undo it later” patterns.

Insert without removing

Pass an empty range and splice becomes a multi‑item insert:

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

v.splice(1..1, [2, 3, 4]);

assert_eq!(v, [1, 2, 3, 4, 5]);

That’s far nicer than calling insert in a loop and watching every push shift the tail again.

When to reach for it

Any time you’d write drain(..) followed by extend(..) or a tower of insert calls, try splice first. One method, one allocation pattern, and the removed items come along for free.

#100 Apr 2026

100. std::cmp::Reverse — Sort Descending Without Writing a Closure

Want to sort a Vec in descending order? The old trick is v.sort_by(|a, b| b.cmp(a)), and every time you write it you flip a and b and pray you got it right. std::cmp::Reverse is a one-word replacement.

The closure swap trick

To sort descending, you reverse the comparison. The closure is short but easy to mess up:

1
2
3
4
let mut scores = vec![30, 10, 50, 20, 40];
scores.sort_by(|a, b| b.cmp(a));

assert_eq!(scores, [50, 40, 30, 20, 10]);

Swap the a and b and you silently get ascending order again. Not a bug the compiler can help with.

Enter Reverse

std::cmp::Reverse<T> is a tuple wrapper whose Ord impl is the reverse of T’s. Hand it to sort_by_key and you’re done:

1
2
3
4
5
6
use std::cmp::Reverse;

let mut scores = vec![30, 10, 50, 20, 40];
scores.sort_by_key(|&s| Reverse(s));

assert_eq!(scores, [50, 40, 30, 20, 10]);

No chance of flipping the comparison the wrong way — the intent is right there in the name.

Sorting by a field, descending

It really shines when you’re sorting structs by a specific field:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::cmp::Reverse;

#[derive(Debug)]
struct Player {
    name: &'static str,
    score: u32,
}

let mut roster = vec![
    Player { name: "Ferris", score: 42 },
    Player { name: "Corro",  score: 88 },
    Player { name: "Rusty",  score: 65 },
];

roster.sort_by_key(|p| Reverse(p.score));

assert_eq!(roster[0].name, "Corro");
assert_eq!(roster[1].name, "Rusty");
assert_eq!(roster[2].name, "Ferris");

Mixing ascending and descending

Sort by one field ascending and another descending? Pack them in a tuple — Reverse composes cleanly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::cmp::Reverse;

let mut items = vec![
    ("apple",  3),
    ("banana", 1),
    ("apple",  1),
    ("banana", 3),
];

// Name ascending, then count descending.
items.sort_by_key(|&(name, count)| (name, Reverse(count)));

assert_eq!(items, [
    ("apple",  3),
    ("apple",  1),
    ("banana", 3),
    ("banana", 1),
]);

It works anywhere Ord does

Because Reverse<T> is an Ord type, you can use it with BinaryHeap to turn a max-heap into a min-heap, or with BTreeSet to iterate in reverse order — no extra wrapper types needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cmp::Reverse;
use std::collections::BinaryHeap;

// BinaryHeap is max-heap by default. Wrap in Reverse for min-heap behaviour.
let mut heap = BinaryHeap::new();
heap.push(Reverse(3));
heap.push(Reverse(1));
heap.push(Reverse(2));

assert_eq!(heap.pop(), Some(Reverse(1)));
assert_eq!(heap.pop(), Some(Reverse(2)));
assert_eq!(heap.pop(), Some(Reverse(3)));

When to reach for it

Any time you’d write b.cmp(a) — reach for Reverse instead. The code reads top-to-bottom, the intent is obvious, and there’s no comparator to accidentally flip.

88. Vec::push_mut — Push and Modify in One Step

Tired of pushing a default into a Vec and then grabbing a mutable reference to fill it in? Rust 1.95 stabilises push_mut, which returns &mut T to the element it just inserted.

The old dance

A common pattern is to push a placeholder value and then immediately mutate it. Before push_mut, you had to do an awkward two-step:

1
2
3
4
5
6
7
8
9
let mut names = Vec::new();

// Push, then index back in to mutate
names.push(String::new());
let last = names.last_mut().unwrap();
last.push_str("hello");
last.push_str(", world");

assert_eq!(names[0], "hello, world");

That last_mut().unwrap() is boilerplate — you know the element is there because you just pushed it. Worse, in more complex code the compiler can’t always prove the reference is safe, forcing you into index gymnastics.

Enter push_mut

push_mut pushes the value and hands you back a mutable reference in one shot:

1
2
3
4
5
6
7
let mut scores = Vec::new();

let entry = scores.push_mut(0);
*entry += 10;
*entry += 25;

assert_eq!(scores[0], 35);

No unwraps, no indexing, no second lookup. The borrow checker is happy because there’s a clear chain of ownership.

Building structs in-place

This really shines when you’re constructing complex values piece by piece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Debug, Default)]
struct Player {
    name: String,
    score: u32,
    active: bool,
}

let mut roster = Vec::new();

let p = roster.push_mut(Player::default());
p.name = String::from("Ferris");
p.score = 100;
p.active = true;

assert_eq!(roster[0].name, "Ferris");
assert_eq!(roster[0].score, 100);
assert!(roster[0].active);

Instead of building the struct fully before pushing, you push a default and fill it in. This can be handy when the final field values depend on context you only have after insertion.

Also works for insert_mut

Need to insert at a specific index? insert_mut follows the same pattern:

1
2
3
4
5
let mut v = vec![1, 3, 4];
let inserted = v.insert_mut(1, 2);
*inserted *= 10;

assert_eq!(v, [1, 20, 3, 4]);

Both methods are also available on VecDeque (push_front_mut, push_back_mut, insert_mut) and LinkedList (push_front_mut, push_back_mut).

When to reach for it

Use push_mut whenever you’d otherwise write push followed by last_mut().unwrap(). It’s one less unwrap, one fewer line, and clearer intent: push this, then let me tweak it.

Stabilised in Rust 1.95 (April 2026) — update your toolchain and give it a spin.

#080 Apr 2026

80. VecDeque::pop_front_if and pop_back_if — Conditional Pops on Both Ends

Vec::pop_if got a deque-shaped sibling. As of Rust 1.93, VecDeque has pop_front_if and pop_back_if — conditional pops on either end without the peek-then-pop dance.

The problem

You want to remove an element from a VecDeque only when it matches a predicate. Before 1.93, you’d peek, match, then pop:

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

let mut queue: VecDeque<i32> = VecDeque::from([1, 2, 3, 4]);

if queue.front().is_some_and(|&x| x == 1) {
    queue.pop_front();
}

Two lookups, two branches, one opportunity to desynchronize the check from the pop if you refactor the predicate later.

The fix

pop_front_if takes a closure, checks the front element against it, and pops it only if it matches. pop_back_if does the same on the other end.

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

let mut queue: VecDeque<i32> = VecDeque::from([1, 2, 3, 4]);

let popped = queue.pop_front_if(|x| *x == 1);
assert_eq!(popped, Some(1));
assert_eq!(queue, VecDeque::from([2, 3, 4]));

// Predicate doesn't match — nothing is popped.
let not_popped = queue.pop_front_if(|x| *x > 100);
assert_eq!(not_popped, None);
assert_eq!(queue, VecDeque::from([2, 3, 4]));

The return type is Option<T>: Some(value) if the predicate matched and the element was removed, None otherwise (including when the deque is empty).

One subtle detail worth noting — the closure receives &mut T, not &T. That means |&x| won’t type-check; use |x| *x == ... or destructure with |&mut x|. The extra flexibility lets you mutate the element in-place before deciding to pop it.

Draining a prefix

The pattern clicks when you pair it with a while let loop. Drain everything at the front that matches a condition, stop the moment it doesn’t:

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

let mut events: VecDeque<i32> = VecDeque::from([1, 2, 3, 10, 11, 12]);

// Drain the "small" prefix only.
while let Some(_) = events.pop_front_if(|x| *x < 10) {}

assert_eq!(events, VecDeque::from([10, 11, 12]));

No index tracking, no split_off, no collecting into a new deque.

Why both ends?

VecDeque is a double-ended ring buffer, so it’s natural to support the same idiom on both sides. Processing a priority queue from the back, trimming expired entries from the front, popping a sentinel only when it’s still there — all one method call each.

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

let mut log: VecDeque<&str> = VecDeque::from(["START", "a", "b", "c", "END"]);

let end = log.pop_back_if(|s| *s == "END");
let start = log.pop_front_if(|s| *s == "START");

assert_eq!(start, Some("START"));
assert_eq!(end, Some("END"));
assert_eq!(log, VecDeque::from(["a", "b", "c"]));

When to reach for it

Whenever the shape of your code is “peek, compare, pop.” That’s the tell. pop_front_if / pop_back_if collapse three steps into one atomic operation, and the Option<T> return makes it composable with while let, ?, and the rest of the Option toolbox.

Stabilized in Rust 1.93 — if your MSRV is recent enough, this is a free readability win.

64. Iterator::unzip — Split Pairs into Separate Collections

Got an iterator of tuples and need two separate collections? Stop looping and pushing manually — unzip splits pairs into two collections in a single pass.

The problem

You have an iterator that yields pairs — maybe key-value tuples from a computation, or results from enumerate. You need two separate Vecs. The manual approach works, but it’s noisy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let pairs = vec![("Alice", 95), ("Bob", 87), ("Carol", 92)];

let mut names = Vec::new();
let mut scores = Vec::new();
for (name, score) in &pairs {
    names.push(*name);
    scores.push(*score);
}

assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
assert_eq!(scores, vec![95, 87, 92]);

Two mutable Vecs, a loop, manual pushes — all to split pairs apart.

The fix

Iterator::unzip does exactly this in one line:

1
2
3
4
5
6
let pairs = vec![("Alice", 95), ("Bob", 87), ("Carol", 92)];

let (names, scores): (Vec<&str>, Vec<i32>) = pairs.into_iter().unzip();

assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
assert_eq!(scores, vec![95, 87, 92]);

The type annotation on the left tells Rust which collections to build. It works with any types that implement Default + Extend — so Vec, String, HashSet, and more.

Works great with enumerate

Need indices and values in separate collections?

1
2
3
4
5
6
let fruits = vec!["apple", "banana", "cherry"];

let (indices, items): (Vec<usize>, Vec<&&str>) = fruits.iter().enumerate().unzip();

assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(items, vec![&"apple", &"banana", &"cherry"]);

Combine with map for transforms

Chain map before unzip to transform on the fly:

1
2
3
4
5
6
7
8
9
let data = vec![("temp_c", 20.0), ("temp_c", 35.0), ("temp_c", 0.0)];

let (labels, fahrenheit): (Vec<&str>, Vec<f64>) = data
    .into_iter()
    .map(|(label, c)| (label, c * 9.0 / 5.0 + 32.0))
    .unzip();

assert_eq!(labels, vec!["temp_c", "temp_c", "temp_c"]);
assert_eq!(fahrenheit, vec![68.0, 95.0, 32.0]);

Unzip into different collection types

Since unzip works with any Default + Extend types, you can collect into mixed collections:

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

let entries = vec![("admin", "read"), ("admin", "write"), ("user", "read")];

let (roles, perms): (Vec<&str>, HashSet<&str>) = entries.into_iter().unzip();

assert_eq!(roles, vec!["admin", "admin", "user"]);
assert_eq!(perms.len(), 2); // "read" and "write", deduplicated
assert!(perms.contains("read"));
assert!(perms.contains("write"));

One side is a Vec, the other is a HashSetunzip doesn’t care, as long as both sides can extend themselves.

Whenever you’re about to write a loop that pushes into two collections, reach for unzip instead.

60. Iterator::partition — Split a Collection in Two

Need to split a collection into two groups based on a condition? Skip the manual loop — Iterator::partition does it in one call.

The manual way

Without partition, you’d loop and push into two separate vectors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];

let mut evens = Vec::new();
let mut odds = Vec::new();

for n in numbers {
    if n % 2 == 0 {
        evens.push(n);
    } else {
        odds.push(n);
    }
}

assert_eq!(evens, vec![2, 4, 6, 8]);
assert_eq!(odds, vec![1, 3, 5, 7]);

It works, but it’s a lot of ceremony for a simple split.

Enter partition

Iterator::partition collects into two collections in a single pass. Items where the predicate returns true go left, false goes right:

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

let (evens, odds): (Vec<_>, Vec<_>) = numbers
    .iter()
    .partition(|&&n| n % 2 == 0);

assert_eq!(evens, vec![&2, &4, &6, &8]);
assert_eq!(odds, vec![&1, &3, &5, &7]);

The type annotation (Vec<_>, Vec<_>) is required — Rust needs to know what collections to build. You can partition into any type that implements Default + Extend, not just Vec.

Owned values with into_iter

Use into_iter() when you want owned values instead of references:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let files = vec![
    "main.rs", "lib.rs", "test_utils.rs",
    "README.md", "CHANGELOG.md",
];

let (rust_files, other_files): (Vec<_>, Vec<_>) = files
    .into_iter()
    .partition(|f| f.ends_with(".rs"));

assert_eq!(rust_files, vec!["main.rs", "lib.rs", "test_utils.rs"]);
assert_eq!(other_files, vec!["README.md", "CHANGELOG.md"]);

A practical use: triaging results

partition pairs beautifully with Result to separate successes from failures:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let inputs = vec!["42", "not_a_number", "7", "oops", "13"];

let (oks, errs): (Vec<_>, Vec<_>) = inputs
    .iter()
    .map(|s| s.parse::<i32>())
    .partition(Result::is_ok);

let values: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
let failures: Vec<_> = errs.into_iter().map(Result::unwrap_err).collect();

assert_eq!(values, vec![42, 7, 13]);
assert_eq!(failures.len(), 2);

partition has been stable since Rust 1.0 — one of those hidden gems that’s been there all along. Anytime you reach for a loop to split items into two buckets, reach for partition instead.

#047 Mar 2026

47. Vec::pop_if — Conditionally Pop the Last Element

Need to remove the last element of a Vec only when it meets a condition? Vec::pop_if does exactly that — no index juggling, no separate check-then-pop.

The old way

Before pop_if, you’d write something like this:

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

if stack.last().is_some_and(|x| *x > 3) {
    let val = stack.pop().unwrap();
    println!("Popped: {val}");
}

Two separate calls, and a subtle TOCTOU gap if you’re not careful — last() checks one thing, then pop() acts on an assumption.

Enter pop_if

Stabilized in Rust 1.86, pop_if combines the check and the removal into one atomic operation:

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

// Pop the last element only if it's greater than 3
let popped = stack.pop_if(|x| *x > 3);
assert_eq!(popped, Some(4));
assert_eq!(stack, [1, 2, 3]);

// Now the last element is 3 — doesn't match, so nothing happens
let stayed = stack.pop_if(|x| *x > 3);
assert_eq!(stayed, None);
assert_eq!(stack, [1, 2, 3]);

The closure receives a &mut T reference to the last element. If it returns true, the element is removed and returned as Some(T). If false (or the vec is empty), you get None.

Mutable access inside the predicate

Because the closure gets &mut T, you can even modify the element before deciding whether to pop it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let mut tasks = vec![
    String::from("buy milk"),
    String::from("URGENT: deploy fix"),
];

let urgent = tasks.pop_if(|task| {
    if task.starts_with("URGENT:") {
        *task = task.replacen("URGENT: ", "", 1);
        true
    } else {
        false
    }
});

assert_eq!(urgent.as_deref(), Some("deploy fix"));
assert_eq!(tasks, vec!["buy milk"]);

A practical use: draining from the back

pop_if is handy for processing a sorted vec from the tail. Think of a priority queue backed by a sorted vec where you only want to process items above a threshold:

1
2
3
4
5
6
7
8
9
let mut scores = vec![10, 25, 50, 75, 90];

let mut high_scores = Vec::new();
while let Some(score) = scores.pop_if(|s| *s >= 70) {
    high_scores.push(score);
}

assert_eq!(high_scores, vec![90, 75]);
assert_eq!(scores, vec![10, 25, 50]);

Clean, expressive, and no off-by-one errors. Another small addition to Vec that makes everyday Rust just a bit nicer.

45. get_disjoint_mut — Multiple Mutable References at Once

The borrow checker won’t let you hold two &mut refs into the same collection — even when you know they don’t overlap. get_disjoint_mut fixes that without unsafe.

The problem

You want to update two elements of the same Vec together, but the compiler won’t allow two mutable borrows at once:

1
2
3
4
let mut scores = vec![10u32, 20, 30, 40];
let a = &mut scores[0];
let b = &mut scores[2]; // ❌ cannot borrow `scores` as mutable more than once
*a += *b;

The borrow checker doesn’t know indices 0 and 2 are different slots — it just sees two &mut to the same Vec. The classic escape hatches (split_at_mut, unsafe, RefCell) all feel like workarounds for something that should just work.

get_disjoint_mut to the rescue

Stabilized in Rust 1.86, get_disjoint_mut accepts an array of indices and returns multiple mutable references — verified at runtime to be non-overlapping:

1
2
3
4
5
6
7
let mut scores = vec![10u32, 20, 30, 40];

if let Ok([a, b]) = scores.get_disjoint_mut([0, 2]) {
    *a += *b; // 10 + 30 = 40
}

assert_eq!(scores, [40, 20, 30, 40]); // ✅

The Result is Err only if an index is out of bounds or indices overlap. Duplicate indices are caught at runtime and return Err — no silent aliasing bugs.

Works on HashMap as well

HashMap gets the same treatment. The return type is [Option<&mut V>; N] — one Option per key, since keys can be missing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::collections::HashMap;

let mut accounts: HashMap<&str, u32> = HashMap::from([
    ("alice", 100),
    ("bob", 200),
]);

// Transfer 50 from bob to alice
let [alice, bob] = accounts.get_disjoint_mut(["alice", "bob"]);
if let (Some(a), Some(b)) = (alice, bob) {
    *a += 50;
    *b -= 50;
}

assert_eq!(accounts["alice"], 150);
assert_eq!(accounts["bob"], 150); // ✅

Passing duplicate keys to the HashMap version panics — the right tradeoff for a bug that would otherwise silently produce undefined behavior.

When to reach for it

  • Swapping or combining two elements in a Vec without split_at_mut gymnastics
  • Updating multiple HashMap entries in one pass
  • Any place you’d have used unsafe or RefCell just to hold two &mut into the same container

If your indices or keys are known not to overlap, get_disjoint_mut is the clean, safe answer.

43. Vec::extract_if — Remove Elements and Keep Them

Ever needed to split a Vec into two groups — the ones you keep and the ones you remove? retain discards the removed items. Now there’s a better way.

Vec::extract_if (stable since Rust 1.87) removes elements matching a predicate and hands them back as an iterator — in a single pass.

The old way — two passes, logic must stay in sync

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

// Collect the evens first…
let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).copied().collect();
// …then remove them (predicate must match exactly)
numbers.retain(|&x| x % 2 != 0);

assert_eq!(numbers, [1, 3, 5]);
assert_eq!(evens,   [2, 4, 6]);

The filter and the retain predicates must be inverses of each other — easy to mistype, and you touch the data twice.

The new way — one pass, one predicate

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

let evens: Vec<i32> = numbers.extract_if(.., |&mut x| x % 2 == 0).collect();

assert_eq!(numbers, [1, 3, 5]);
assert_eq!(evens,   [2, 4, 6]);

extract_if walks the Vec, removes every element where the closure returns true, and yields it. The .. is a range — you can narrow it to only consider a slice of the vector.

Real-world example: draining a work queue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[derive(Debug)]
struct Job { id: u32, priority: u8 }

let mut queue = vec![
    Job { id: 1, priority: 3 },
    Job { id: 2, priority: 9 },
    Job { id: 3, priority: 1 },
    Job { id: 4, priority: 8 },
];

// Pull out all high-priority jobs for immediate processing
let urgent: Vec<Job> = queue.extract_if(.., |j| j.priority >= 8).collect();

assert_eq!(urgent.len(), 2);  // jobs 2 and 4
assert_eq!(queue.len(),  2);  // jobs 1 and 3 remain

HashMap and HashSet also gained extract_if in Rust 1.88.

Note: The closure takes &mut T, so you can even mutate elements mid-extraction before deciding whether to remove them.

31. HashMap's entry API

Want to insert a value into a HashMap only if the key doesn’t exist yet? Skip the double lookup — use the entry API.

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

let mut scores: HashMap<&str, Vec<u32>> = HashMap::new();

// Instead of checking .contains_key() then inserting:
scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(100);

scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(200);

assert_eq!(scores["alice"], vec![100, 200]);

The entry API returns an Entry enum — either Occupied or Vacant. The convenience methods make common patterns a one-liner:

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

let mut word_count: HashMap<&str, usize> = HashMap::new();
let words = ["hello", "world", "hello", "rust", "hello"];

for word in words {
    *word_count.entry(word).or_insert(0) += 1;
}

// hello => 3, world => 1, rust => 1

or_insert(val) inserts a default, or_insert_with(|| val) lazily computes it, and or_default() uses the type’s Default. All three return a mutable reference to the value, so you can update in place.