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
inspect(f) as a transparent pass-through that applies a side-effect to each elementinspect() to debug intermediate values inside a lazy iterator pipelineinspect() does not modify values — it only observes theminspect() to count elements processed at different pipeline stages for profilingCode 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
inspect() as a standard adapter; OCaml requires a manual tap combinator.inspect() only fires when the pipeline is consumed — which reveals evaluation order and lazy behavior.inspect() can collect observed values into a Vec via closure capture, enabling assertion-based pipeline testing.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
inspect() calls to a multi-step iterator pipeline to print each stage's output, then count how many elements reach each stage.inspect() with a mutable counter to count how many elements pass through each stage of a filter().map().take_while() pipeline.inspect() to capture intermediate values into a Vec via closure capture and assert their expected values.