ExamplesBy LevelBy TopicLearning Paths
261 Intermediate

261: Lookahead with Peekable

Functional Programming

Tutorial

The Problem

Many parsing and grouping algorithms need to examine the next element before deciding whether to consume it. A lexer must decide if the current < starts <, <=, or << by looking ahead. A run-length encoder must check if the next element continues the current run. Without lookahead, you must buffer elements manually or restructure the algorithm. Peekable wraps any iterator to add a one-element lookahead without consuming the peeked element.

🎯 Learning Outcomes

  • • Understand how peek() inspects the next element without advancing the iterator
  • • Use Peekable to implement run-length grouping and lexer-style tokenization
  • • Recognize that peek() returns Option<&Item> — a reference to the next element
  • • Combine peek() with while let to consume elements conditionally
  • Code Example

    #![allow(clippy::all)]
    //! 261. Lookahead with Peekable
    //!
    //! `Peekable` adds `peek()` to inspect the next element without consuming it.
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_peek_no_consume() {
            let mut iter = [1i32, 2, 3].iter().peekable();
            let p = iter.peek().copied().copied();
            let n = iter.next().copied();
            assert_eq!(p, Some(1));
            assert_eq!(n, Some(1));
            assert_eq!(iter.next().copied(), Some(2));
        }
    
        #[test]
        fn test_peek_groups() {
            let data = [1i32, 1, 2, 3, 3];
            let mut iter = data.iter().peekable();
            let mut groups: Vec<Vec<i32>> = Vec::new();
            while let Some(&val) = iter.peek() {
                let mut g = Vec::new();
                while iter.peek() == Some(&val) {
                    g.push(*iter.next().unwrap());
                }
                groups.push(g);
            }
            assert_eq!(groups, vec![vec![1, 1], vec![2], vec![3, 3]]);
        }
    
        #[test]
        fn test_peek_empty() {
            let data: Vec<i32> = vec![];
            let mut p = data.iter().peekable();
            assert_eq!(p.peek(), None);
        }
    }

    Key Differences

  • Built-in support: Rust provides Peekable<I> as a standard adapter; OCaml requires manual buffering.
  • Reference semantics: peek() returns Option<&Item> — a reference — so the item remains available for next(); no copying occurs.
  • One-element buffer: Peekable buffers exactly one element; for deeper lookahead you need a different approach.
  • Parser combinators: Libraries like nom and pest build on this pattern for full recursive-descent parsing.
  • OCaml Approach

    OCaml's standard library does not provide a built-in peekable abstraction. The idiomatic approach is to use a ref holding an optional buffered element, or to restructure the algorithm to pass the "next" element as an argument to recursive calls:

    (* Manual lookahead with a buffer ref *)
    let peekable seq =
      let buf = ref None in
      let peek () = match !buf with
        | Some v -> Some v
        | None -> let v = Seq.uncons seq in buf := Option.map fst v; Option.map fst v
      in ...
    

    Full Source

    #![allow(clippy::all)]
    //! 261. Lookahead with Peekable
    //!
    //! `Peekable` adds `peek()` to inspect the next element without consuming it.
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_peek_no_consume() {
            let mut iter = [1i32, 2, 3].iter().peekable();
            let p = iter.peek().copied().copied();
            let n = iter.next().copied();
            assert_eq!(p, Some(1));
            assert_eq!(n, Some(1));
            assert_eq!(iter.next().copied(), Some(2));
        }
    
        #[test]
        fn test_peek_groups() {
            let data = [1i32, 1, 2, 3, 3];
            let mut iter = data.iter().peekable();
            let mut groups: Vec<Vec<i32>> = Vec::new();
            while let Some(&val) = iter.peek() {
                let mut g = Vec::new();
                while iter.peek() == Some(&val) {
                    g.push(*iter.next().unwrap());
                }
                groups.push(g);
            }
            assert_eq!(groups, vec![vec![1, 1], vec![2], vec![3, 3]]);
        }
    
        #[test]
        fn test_peek_empty() {
            let data: Vec<i32> = vec![];
            let mut p = data.iter().peekable();
            assert_eq!(p.peek(), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_peek_no_consume() {
            let mut iter = [1i32, 2, 3].iter().peekable();
            let p = iter.peek().copied().copied();
            let n = iter.next().copied();
            assert_eq!(p, Some(1));
            assert_eq!(n, Some(1));
            assert_eq!(iter.next().copied(), Some(2));
        }
    
        #[test]
        fn test_peek_groups() {
            let data = [1i32, 1, 2, 3, 3];
            let mut iter = data.iter().peekable();
            let mut groups: Vec<Vec<i32>> = Vec::new();
            while let Some(&val) = iter.peek() {
                let mut g = Vec::new();
                while iter.peek() == Some(&val) {
                    g.push(*iter.next().unwrap());
                }
                groups.push(g);
            }
            assert_eq!(groups, vec![vec![1, 1], vec![2], vec![3, 3]]);
        }
    
        #[test]
        fn test_peek_empty() {
            let data: Vec<i32> = vec![];
            let mut p = data.iter().peekable();
            assert_eq!(p.peek(), None);
        }
    }

    Exercises

  • Use Peekable to implement a simple tokenizer that merges consecutive digit characters into a single number token.
  • Build a group_consecutive function that groups adjacent equal elements without allocating a combined collection first.
  • Use peek() to implement a merge_sorted function that merges two sorted iterators into one sorted output without a temporary buffer.
  • Open Source Repos