#203 Jun 15, 2026

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

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

The trap: the peek / compute / advance dance

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

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

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

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

The fix: decide and transform in one call

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

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

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

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

It really does put the item back

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

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

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

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

Where it shines: tokenizing

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

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

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

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

The bottom line

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

← Previous 202. Arc::clone Is a Refcount Bump, Not a Deep Copy — Share Big Data, Don't Duplicate It Next → 204. take_while / skip_while — Act on the Leading Run, Not Every Match