#129 May 10, 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.

← Previous 128. slice::copy_within — Shift Bytes In-Place Without Fighting the Borrow Checker