Introduction
An explicit for loop is often the clearest way to process a collection. Rust also provides iterators: values that produce one item at a time and can be combined into a readable data-processing pipeline.
You will keep a working loop as a baseline, reproduce its total with iter and sum, write a small closure, transform readings with map, reject invalid readings with filter, and finally compose the operations. Every edit is made in a prepared Cargo project, so you can focus on the new Rust concepts.
Reproduce a Loop Total with an Iterator
In this step, you will compare the prepared for loop with an iterator that computes the same total.
The project is /home/labex/project/reading-pipeline, and its program is in src/main.rs. The prepared vec![12, 18, -1, 24, 30] uses the vec! macro as a compact way to create a vector containing those values; it is equivalent in purpose to creating an empty vector and pushing each value in the earlier Vec Lab. The existing loop borrows that vector with &readings, visits each &i32, and uses *reading to access the integer behind that reference.
Enter the project and run the baseline:
cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83
The total includes every reading, including -1. Now open the source:
nano src/main.rs
Replace the Step 1 comment with:
let iterator_total: i32 = readings.iter().sum();
println!("Iterator total: {iterator_total}");
readings.iter() creates an iterator that borrows each vector element. It does not remove or consume the vector. sum() repeatedly adds the produced items; Rust's standard library knows how to add borrowed integers. The annotation : i32 states the result type that sum should build.
Save with Ctrl+O, press Enter, and exit with Ctrl+X. Check the code before running it:
cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83
The matching lines show that the loop and iterator describe the same computation. The vector remains available for later steps because iter() only borrowed it.
Transform Readings with a Closure and Map
In this step, you will define a closure and apply it to every reading with map.
A closure is a small unnamed function that can be stored in a variable or passed directly to an iterator adapter. The form |parameter: Type| expression places parameters between vertical bars and returns the expression's value. Here, a calibration closure will add an offset of two.
Open the source:
nano src/main.rs
Replace the Step 2 comment with:
let add_offset = |reading: i32| reading + 2;
let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
println!("Adjusted all: {adjusted:?}");
map calls its closure once for every iterator item and produces the returned values. The type flow is &i32 from iter() → copied i32 from *reading → adjusted i32 from add_offset → elements of Vec<i32> from collect(). The :? formatter prints the vector in a readable debug form.
Save and exit, then check and run:
cargo check
cargo run --quiet
The new final line should be:
Adjusted all: [14, 20, 1, 26, 32]
Even the invalid -1 was mapped to 1; map transforms items but does not decide which items belong.
Keep Only Valid Readings with Filter
In this step, you will use filter to keep nonnegative readings and leave the original vector unchanged.
An iterator's filter adapter calls a predicate closure for each candidate. A predicate returns true to keep an item or false to reject it. Open the source:
nano src/main.rs
Replace the Step 3 comment with:
let valid: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.collect();
println!("Valid only: {valid:?}");
Method chains can continue on indented lines after a dot. iter() borrows the vector, and copied() turns each borrowed &i32 item into an i32; this is inexpensive because integers implement Copy. The filter predicate then receives a temporary reference to each i32 candidate so it can inspect without consuming it, which is why the comparison uses *reading. The type flow is &i32 → copied i32 → kept i32 → Vec<i32>.
Save and exit, then run:
cargo run --quiet
The final line should be:
Valid only: [12, 18, 24, 30]
The negative value is absent, while the retained values remain in their original order.
Compose a Complete Processing Pipeline
In this step, you will chain filtering and mapping, collect the processed readings, and sum that result.
Iterator adapters are lazy: filter and map describe work, but items flow through them only when a consuming operation asks for results. collect and sum are consuming operations. They consume the iterator pipeline, not the borrowed source vector.
Open the source:
nano src/main.rs
Replace the Step 4 comment with:
let valid_adjusted: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.map(add_offset)
.collect();
let valid_total: i32 = valid_adjusted.iter().sum();
println!("Valid adjusted: {valid_adjusted:?}");
println!("Valid adjusted total: {valid_total}");
The pipeline first rejects -1, then map(add_offset) passes each remaining i32 to the closure you defined earlier. A closure name can be supplied directly when its parameter and return types fit the adapter. The second iterator borrows the collected vector and sums it.
Save and exit, then check and run:
cargo check
cargo run --quiet
The last two lines should be:
Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92
The order of operations matters: filtering before mapping prevents the invalid reading from becoming an apparently valid positive value. The original readings vector was only borrowed and could still be used afterward.
Summary
You compared an explicit loop with an iterator, wrote and reused a closure, transformed values with map, selected values with filter, collected a new vector, and calculated a total with sum. You also preserved the source vector by beginning each pipeline with a borrowed iterator.


