Rust Iterators and Lazy Adapters¶
An iterator supplies items through next(); adapters such as map and filter describe how future items will be processed. Building a chain does not run its callbacks, and choosing iter(), iter_mut() or into_iter() determines what that particular collection lends or transfers.
Prerequisites and outcome¶
Complete closures and function pointers. You will track the item type through an adapter chain, choose when to consume it, implement a small Iterator, and preserve parsing failures during collection. All inputs are fixtures; no live sensor is used.
Rust Iterator is a standard-library trait¶
Iterator declares an associated Item type and fn next(&mut self) -> Option<Self::Item>. Our Countdown supplies owned u8 values and updates its state on each call. Its zero state returns None without subtracting, avoiding unsigned underflow.
The language's for loop uses IntoIterator to obtain an iterator, then repeatedly obtains items. Iterator and IntoIterator are library interfaces, while the loop syntax is a language construct. See iterator loops and IntoIterator.
Build the complete iterator program¶
Keep edition = "2024" in Cargo.toml and replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
Expected output:
There are eight tests. The lazy tests record which inputs were visited without timing measurements or interior mutability. Dropping a map pipeline releases its captures without calling its mapping closure.
Track the item type, not just the method name¶
| Expression on our Vec |
Item | Effect on collection ownership |
|---|---|---|
| readings.iter() | &i32 | Shared borrow |
| readings.iter_mut() | &mut i32 | Exclusive borrow |
| readings.into_iter() | i32 | Consumes the Vec |
| readings.iter().copied() | i32 | Copies each integer, preserving the Vec |
These rows describe Vec, not a universal rule that every into_iter call consumes backing data. References also implement IntoIterator; for value in &readings borrows through that implementation. For non-Copy owned elements such as String, into_iter can transfer each element; copied() is not available for String.
In high_readings, copied changes the item from &i32 to i32. filter then passes a shared reference to that item into its predicate, explaining the dereference in |value| *value >= threshold. The opaque iterator borrows readings, while its move closure owns a copy of threshold; move does not make the borrowed slice static.
Lazy adapters still take ownership of iterator state¶
map, filter and take return iterator adapters whose callbacks run when items are requested. collect, sum and count consume an iterator to produce a result; a for loop also requests items. Creating an unused map does not perform a transformation, even if the closure contains visible side effects. See the Book's iterator progression.
Laziness does not mean “no ownership change”. An adapter may take its input iterator by value while deferring processing of items. sum consumes its receiver too, so reusing that same iterator binding afterward can be a move error. Building an iterator can also evaluate arguments or move captures immediately; only the deferred item processing is lazy.
collect's target determines the result type. We annotate Veccollect::<Vec<_>>(); the underscore asks for an inferred element type. Do not assume every collect returns a Vec or that all consumers visit every input: take limits demand, and short-circuiting consumers may stop early.
For advanced traversal, next advances state directly, by_ref lends an iterator to an adapter, and fold accumulates without necessarily allocating a result collection. Consult the Iterator API when choosing between transforming, borrowing and consuming methods.
Keep failure policy explicit¶
parse_all maps each input to Result
Using filter_map with Result::ok deliberately discards failed records. It does not count invalid inputs, distinguish missing values, or preserve their error details. That is a different policy, shown separately so it cannot silently replace the Part I report's accounting. For a report that must keep all errors, retain the Results or accumulate measured/missing/invalid categories explicitly.
Implementing next does not promise every optimisation¶
Countdown's Item is u8 and its state is a remaining count. Once it reaches zero, this particular implementation keeps returning None. Iterator in general permits a later Some after None; use fuse() or a suitable FusedIterator guarantee when repeated exhaustion matters.
An Iterator implementation does not automatically promise an exact remaining length, double-ended traversal, or a particular allocation strategy. Additional traits and size_hint contracts have their own requirements. Do not claim a custom iterator is faster than a loop merely because it has fewer source lines.
Deliberately failing: using a consumed Vec¶
In a separate scratch project, replace src/main.rs with:
cargo check reports E0382 because into_iter consumes this Vec. To preserve it, use a borrowed iterator and copy the integer items, for example readings.iter().copied().sum(). Do not clone the entire collection when a shared traversal is sufficient.
Exercises and troubleshooting¶
- Repair the consumed-Vec example with iter().copied(): expect
sum=106700, length=3. Notice that these values were not incremented like the main program's readings. - Remove the dereference in high_readings' filter predicate: expect E0308 because the predicate receives &i32 while threshold is i32.
- Store
let values = readings.iter();, call values.sum::(), then try values.count(): expect E0382 because sum consumes the iterator, even though the Vec is still borrowed rather than consumed. - Change parse_all to filter out unsuccessful parses and return Ok of the remaining values: the invalid-input tests must fail. Explain the lost error policy rather than updating the tests to accept missing records.
- Change Countdown's initial remaining value in main from 3 to 0: expect
countdown=[]. Its zero/exhaustion tests should still pass. - In the lazy take test, use take(2): expect output
[2, 3]and visited[1, 2]. Construction alone should still visit no inputs.
If collect cannot infer a type, annotate its target. If a callback argument is a reference, follow the current Item and the adapter's signature. If a pipeline silently loses data, inspect filter_map, flatten and error conversion rather than assuming laziness is responsible.
Verification and next step¶
On October 10, 2026, the lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all eight tests, formatting and debug/release output comparisons passed. Borrowed-sum repair, zero countdown and two-item demand variants also passed. Consumed Vec/iterator and incorrectly referenced filter arguments produced E0382 or E0308 as expected. Replacing strict parsing with error discarding compiled but failed the invalid-input test, confirming that policy change was detected. No performance or live sensor claim is made.
Next: tests, documentation and Cargo, including the Part II reusable-library checkpoint.