Iterators vs streams
In one sentence
Section titled “In one sentence”An iterator chain such as orders.iter().filter(...).map(...).collect() is a Java stream pipeline: lazy, with collect() (or sum(), count(), a for loop) as the terminal operation.
The Java you know
Section titled “The Java you know”Java
/** The IDs of paid orders over 100.00, largest total first. */static List<Long> bigPaidOrders(List<Order> orders) { return orders.stream() .filter(o -> o.paid() && o.totalCents() > 10_000) .sorted(Comparator.comparingLong(Order::totalCents).reversed()) .map(Order::id) .toList();}Rust
/// The IDs of paid orders over 100.00, largest total first.pub fn big_paid_orders(orders: &[Order]) -> Vec<u64> { let mut big: Vec<&Order> = orders .iter() .filter(|o| o.paid && o.total_cents > 10_000) .collect(); big.sort_by_key(|o| Reverse(o.total_cents)); // largest first big.iter().map(|o| o.id).collect()}The shape is the same: a source, then adapters (filter, map), then a terminal operation. Two differences are visible here:
iter()instead ofstream(). The name says how the items are given: here as&Order, borrowed.- The sort is not in the chain. Rust has no
sorted()adapter in the standard library, because a sort needs all the items. The code collects into aVec, sorts it, and starts a second chain.
The Rust you will see
Section titled “The Rust you will see”iter(), iter_mut(), into_iter()
Section titled “iter(), iter_mut(), into_iter()”This is the most important thing to read in an iterator chain. Each method gives the items in a different way:
pub fn three_ways(mut orders: Vec<Order>) -> (u64, Vec<String>) { // `iter()`: borrow each item. `orders` is still usable after this line. let total: u64 = orders.iter().map(|o| o.total_cents).sum();
// `iter_mut()`: borrow each item mutably, to change it in place. // `for x in &mut orders` is the short form of `orders.iter_mut()`. for order in &mut orders { order.paid = true; }
// `into_iter()`: take ownership of each item. `orders` is gone after this. let customers = orders.into_iter().map(|o| o.customer).collect();
(total, customers)}- Line 3:
iter()gives&Order: a shared borrow of each item. TheVecis unchanged and still usable. - Line 7:
&mut ordersin aforloop callsiter_mut(). It gives&mut Order, so the loop can change each item in place. - Line 12:
into_iter()givesOrderby value. It consumes theVec: after this line,orderscannot be used. The closure can moveo.customerout, with no clone.
| You write | Items are | The collection after |
|---|---|---|
v.iter() or for x in &v |
&T |
unchanged |
v.iter_mut() or for x in &mut v |
&mut T |
changed in place |
v.into_iter() or for x in v |
T |
gone (moved) |
When you see .clone() inside a chain, look at which of the three the chain starts with. Often into_iter() removes the clone, or iter() and a borrow are enough (see Ownership and moves).
Lazy, like streams
Section titled “Lazy, like streams”An adapter does nothing on its own. It builds a description of the work. The work runs when something asks for items: next(), collect(), sum(), or a for loop.
pub fn first_big_order(orders: &[Order], checked: &mut usize) -> Option<u64> { let mut big = orders .iter() .inspect(|_| *checked += 1) // counts how many orders the chain looks at .filter(|o| o.total_cents > 10_000) .map(|o| o.id); // nothing has run yet: `big` is only a description
big.next() // runs the chain until it finds one match, then stops}The test chains_are_lazy proves that the chain looks at one order only: next() stops at the first match. A Java stream with findFirst() behaves the same way.
How to read it
Section titled “How to read it”collect() and the turbofish
Section titled “collect() and the turbofish”collect() can build many types: a Vec, a HashMap, a String, a HashSet. It needs to know which. The type comes from the variable (let v: Vec<u64> = ...collect();), the return type of the function, or a turbofish: .collect::<Vec<_>>(). The _ means “infer the item type”.
collect() can also build a Result. An iterator of Result<T, E> collects into Result<Vec<T>, E>: either all the values, or the first error:
/// Parse every line, or return the first error.pub fn parse_quantities(lines: &[&str]) -> Result<Vec<u32>, ParseIntError> { lines .iter() .map(|line| line.trim().parse::<u32>()) .collect()}Java streams have no equivalent. In Java, a checked exception inside a lambda is a well-known problem. In Rust, the error is a value, and collect stops at the first one. The same works with Option.
Adapters you will see
Section titled “Adapters you will see”pub fn adapters(prices: &[u64], names: &[&str]) { let numbered: Vec<(usize, &u64)> = prices.iter().enumerate().collect(); // (0, p0), (1, p1), ... let labelled: Vec<String> = names .iter() .zip(prices) // pairs, stops at the shorter one .map(|(n, p)| format!("{n}={p}")) .collect(); let rising = prices.windows(2).all(|w| w[0] <= w[1]); // overlapping pairs let pages: Vec<&[u64]> = prices.chunks(2).collect(); // groups of 2, last may be shorter let even: Vec<u64> = prices.iter().copied().filter(|p| p % 2 == 0).collect(); let parsed: Vec<u64> = names.iter().filter_map(|n| n.parse().ok()).collect(); // drop the failures let chars: Vec<char> = names.iter().flat_map(|n| n.chars()).collect(); // one level flatter let first_expensive = prices.iter().position(|&p| p > 1_000); // Option<usize> let any_expensive = prices.iter().any(|&p| p > 5_000); let (count, sum) = prices.iter().fold((0, 0), |(n, s), p| (n + 1, s + p)); // like `reduce`}| Rust | Java stream | Note |
|---|---|---|
map, filter |
map, filter |
|
filter_map |
map then filter(Objects::nonNull) |
the closure returns an Option |
flat_map |
flatMap |
|
enumerate |
(an index variable) | gives (index, item) |
zip |
(none) | pairs two iterators |
take, skip, take_while |
limit, skip, takeWhile |
|
any, all, find, position |
anyMatch, allMatch, findFirst |
stop at the first answer |
sum, count, min, max |
sum, count, min, max |
|
fold |
reduce with an identity |
|
copied, cloned |
(none) | &u64 to u64, &T to T |
windows(n), chunks(n) |
(none) | on slices, not on iterators |
for loops are idiomatic too
Section titled “for loops are idiomatic too”A chain is not always better. When the body has several steps, early exits, or ?, a for loop is often clearer:
/// A `for` loop is clearer when the body has several steps and an early exit.pub fn reserve_stock(stock: &mut [u32], wanted: &[(usize, u32)]) -> Result<(), usize> { for &(item, count) in wanted { let Some(available) = stock.get_mut(item) else { return Err(item); }; if *available < count { return Err(item); } *available -= count; } Ok(())}In review, do not ask for chains everywhere. Ask for the version that is easiest to read. A for loop over the items (for x in &items) is fine. A for loop over indexes (for i in 0..items.len()) usually is not. See S10: Index-based loops instead of iterators.
What good looks like
Section titled “What good looks like”- The right start:
iter()to read,iter_mut()to change,into_iter()when the collection is not needed after. - Short chains of three to five steps, each on its own line.
- The terminal operation that says the intent:
anyinstead offilter(...).count() > 0,suminstead of a manualfold. - No
collect()in the middle unless the next step needs all the items (sorting, or two passes). forloops when the body is long or has early exits.
Red flags in AI output
Section titled “Red flags in AI output”- Index loops:
for i in 0..v.len()withv[i]. See S10: Index-based loops instead of iterators. collect(), then iterate again, orcollect()only to call.len()or.is_empty(). See S11:collect()then iterate again..iter().map(|x| x.clone())instead of.cloned(), or.iter().cloned()whereinto_iter()gives owned values for free..count() > 0or.len() > 0to test for “any”. Useany()oris_empty().- Very long chains with nested closures,
foldwith tuples, orflat_mapinsidemap. Ask for named steps or a loop.
- S10Severity: LowIndex-based loops instead of iterators
for i in 0..v.len()withv[i], where an iterator is clearer and has no bounds checks. - S11Severity: Low
collect()then iterate againAn intermediate collection that allocates for no reason.
Better vs worse
Section titled “Better vs worse”The task: the names of customers with paid orders, and whether any order is unpaid.
WorseAn index loop, a
&Vecparameter, aVecof cloned orders built only to calllen().pub fn customer_report(orders: &Vec<Order>) -> (Vec<String>, bool) {let mut names = Vec::new();for i in 0..orders.len() {if orders[i].paid {names.push(orders[i].customer.clone());}}let unpaid: Vec<Order> = orders.iter().filter(|o| !o.paid).map(|o| o.clone()).collect();let has_unpaid = unpaid.len() > 0;(names, has_unpaid)}Run in Playground (opens the Rust Playground in a new tab) BetterA
filterandmapchain for the names, andanyfor the check.pub fn customer_report(orders: &[Order]) -> (Vec<String>, bool) {let names = orders.iter().filter(|o| o.paid).map(|o| o.customer.clone()).collect();let has_unpaid = orders.iter().any(|o| !o.paid);(names, has_unpaid)}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version gives the same result, as the test both_reports_agree shows. But it clones every unpaid order, and it keeps them in a Vec, only to ask if the Vec is empty. any stops at the first unpaid order and copies nothing. Clippy’s default lints report four problems in the “worse” code: needless_range_loop, map_clone, len_zero, and ptr_arg. The nursery lint needless_collect reports the Vec that is only used for its length.
Check yourself
Section titled “Check yourself”What is the difference between
orders.iter()andorders.into_iter()on aVec<Order>?Show the answer
iter()gives&Orderand leaves theVecusable.into_iter()givesOrderby value and consumes theVec, so it cannot be used afterwards.A chain has
.map(...)but nocollect,sum,forloop, ornext. What does it do?Show the answer
Nothing. Iterators are lazy. The compiler warns about it: an unused iterator adapter is a
must_usewarning.What does
lines.iter().map(|l| l.parse::<u32>()).collect::<Result<Vec<_>, _>>()return for one bad line?Show the answer
Errwith the parse error of the first bad line. If all lines parse, it isOkwith all the numbers.An agent wrote
items.iter().filter(|i| i.urgent).count() > 0. What do you suggest?Show the answer
items.iter().any(|i| i.urgent). It says the intent, and it stops at the first urgent item.