Peekable

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.

52. Peekable::next_if_map — Peek, Match, and Transform in One Step

Tired of peeking at the next element, checking if it matches, and then consuming and transforming it? next_if_map collapses that entire dance into a single call.

The problem

When writing parsers or processing token streams, you often need to conditionally consume the next element and extract something from it. With next_if and peek, you end up doing the work twice — once to check, once to transform:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clunky: peek, check, consume, transform — separately
let val = if iter.peek().is_some_and(|n| *n > 0) {
    iter.next().map(|n| n * 10)
} else {
    None
};

assert_eq!(val, Some(10));

It works, but you’re expressing the same logic in two places and the code doesn’t clearly convey its intent.

Enter next_if_map

Stabilized in Rust 1.94, Peekable::next_if_map takes a closure that returns Result<R, I::Item>. Return Ok(transformed) to consume the element, or Err(original) to put it back:

1
2
3
4
5
6
7
8
9
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clean: one closure handles the check AND the transformation
let val = iter.next_if_map(|n| {
    if n > 0 { Ok(n * 10) } else { Err(n) }
});

assert_eq!(val, Some(10));
assert_eq!(iter.next(), Some(2)); // iterator continues normally

The element is only consumed when you return Ok. If you return Err, the original value goes back and the iterator is unchanged.

Parsing example: extract leading digits

This is where next_if_map really shines — pulling typed tokens out of a character stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut chars = "42abc".chars().peekable();

let mut number = 0u32;
while let Some(digit) = chars.next_if_map(|c| {
    c.to_digit(10).ok_or(c)
}) {
    number = number * 10 + digit;
}

assert_eq!(number, 42);
assert_eq!(chars.next(), Some('a')); // non-digit stays unconsumed

Each character is inspected once: digits are consumed and converted, and the first non-digit stops the loop without being eaten.

Key details

  • Atomic peek + consume + transform: no redundant checks, no repeated logic
  • Non-destructive on rejection: returning Err(item) puts the element back
  • Also available: next_if_map_mut takes FnOnce(&mut I::Item) -> Option<R> for when you don’t need ownership
  • Stable since Rust 1.94

Next time you’re writing a peek-then-consume pattern, reach for next_if_map — your parser will thank you.