ExamplesBy LevelBy TopicLearning Paths
273 Intermediate

273: Debugging Iterators with inspect()

Functional Programming

Tutorial

The Problem

Iterator pipelines are often opaque: when a filter().map().fold() chain produces an unexpected result, there is no obvious place to insert a print statement without breaking the pipeline. The inspect() adapter solves this by injecting a side-effect function at any point in the pipeline — the values pass through unchanged, but the closure can log, count, assert, or monitor them. It is the functional equivalent of console.log placed between transformations.

🎯 Learning Outcomes

  • • Understand inspect(f) as a transparent pass-through that applies a side-effect to each element
  • • Use inspect() to debug intermediate values inside a lazy iterator pipeline
  • • Recognize that inspect() does not modify values — it only observes them
  • • Apply inspect() to count elements processed at different pipeline stages for profiling
  • Code Example

    #![allow(clippy::all)]
    //! 273. Debugging iterators with inspect()
    //!
    //! `inspect(f)` taps into an iterator pipeline with a side-effect, passing values unchanged.
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_inspect_no_change() {
            let result: Vec<i32> = [1, 2, 3].iter().copied().inspect(|_| {}).collect();
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_side_effect() {
            let mut seen = Vec::new();
            let _result: Vec<i32> = [1, 2, 3]
                .iter()
                .copied()
                .inspect(|&x| seen.push(x))
                .collect();
            assert_eq!(seen, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_between_stages() {
            let mut after_filter = Vec::new();
            let result: Vec<i32> = (1..=6)
                .filter(|&x| x % 2 == 0)
                .inspect(|&x| after_filter.push(x))
                .map(|x| x * 10)
                .collect();
            assert_eq!(after_filter, vec![2, 4, 6]);
            assert_eq!(result, vec![20, 40, 60]);
        }
    }

    Key Differences

  • Built-in vs manual: Rust provides inspect() as a standard adapter; OCaml requires a manual tap combinator.
  • Laziness reveals timing: Because Rust iterators are lazy, inspect() only fires when the pipeline is consumed — which reveals evaluation order and lazy behavior.
  • Testing use: inspect() can collect observed values into a Vec via closure capture, enabling assertion-based pipeline testing.
  • Production use: inspect() with logging frameworks (like tracing) is used in production pipelines to add observability without restructuring code.
  • OCaml Approach

    OCaml lacks a built-in inspect equivalent. The idiomatic approach wraps a function with a side-effect using |> and a tap-like helper:

    let tap f x = f x; x  (* apply side-effect, return value unchanged *)
    
    let result =
      [1;2;3;4;5]
      |> List.map (tap (Printf.printf "before filter: %d\n"))
      |> List.filter (fun x -> x mod 2 = 0)
      |> List.map (tap (Printf.printf "after filter: %d\n"))
    

    The tap pattern is standard in functional languages for inserting side-effects into pipelines.

    Full Source

    #![allow(clippy::all)]
    //! 273. Debugging iterators with inspect()
    //!
    //! `inspect(f)` taps into an iterator pipeline with a side-effect, passing values unchanged.
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_inspect_no_change() {
            let result: Vec<i32> = [1, 2, 3].iter().copied().inspect(|_| {}).collect();
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_side_effect() {
            let mut seen = Vec::new();
            let _result: Vec<i32> = [1, 2, 3]
                .iter()
                .copied()
                .inspect(|&x| seen.push(x))
                .collect();
            assert_eq!(seen, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_between_stages() {
            let mut after_filter = Vec::new();
            let result: Vec<i32> = (1..=6)
                .filter(|&x| x % 2 == 0)
                .inspect(|&x| after_filter.push(x))
                .map(|x| x * 10)
                .collect();
            assert_eq!(after_filter, vec![2, 4, 6]);
            assert_eq!(result, vec![20, 40, 60]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_inspect_no_change() {
            let result: Vec<i32> = [1, 2, 3].iter().copied().inspect(|_| {}).collect();
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_side_effect() {
            let mut seen = Vec::new();
            let _result: Vec<i32> = [1, 2, 3]
                .iter()
                .copied()
                .inspect(|&x| seen.push(x))
                .collect();
            assert_eq!(seen, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_inspect_between_stages() {
            let mut after_filter = Vec::new();
            let result: Vec<i32> = (1..=6)
                .filter(|&x| x % 2 == 0)
                .inspect(|&x| after_filter.push(x))
                .map(|x| x * 10)
                .collect();
            assert_eq!(after_filter, vec![2, 4, 6]);
            assert_eq!(result, vec![20, 40, 60]);
        }
    }

    Exercises

  • Add inspect() calls to a multi-step iterator pipeline to print each stage's output, then count how many elements reach each stage.
  • Use inspect() with a mutable counter to count how many elements pass through each stage of a filter().map().take_while() pipeline.
  • Write a test that uses inspect() to capture intermediate values into a Vec via closure capture and assert their expected values.
  • Open Source Repos