Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to repeat work over a collection

loop, while, for, iterator adapters and consumers, collect, your own Iterator, and par_iter. Which one reads best for the job.

Ask what the loop is for.

  • Repeat until something happens, with no collection involved (retry until a connection works, read until end of input) → loop with break, or while.
  • Do something with each item (print it, send it) → a for loop. Borrow with for x in &v if you need v afterwards; for x in v consumes it.
  • Turn one collection into a value or another collection (sum the prices, keep the valid lines, build a map) → an iterator chain: adapters (map, filter, take) then one consumer (sum, collect, find, fold).
  • The loop body has early returns, several outputs, or error handling that fights the chain → go back to a for loop. Both compile to the same speed; pick the one that reads.
  • The sequence is computed, not stored (Fibonacci, the lines of a stream, a tree walk) → your own Iterator, or iter::from_fn / successors for a quick one.
  • Every item's work is independent and there's a lot of it → rayon's par_iter, the same chain spread across cores.

Plain loops

The three loop forms, and loop as an expression that returns a value.

Leave or skip, including out of an outer loop.

Consume, borrow, or change each element in place.

Iterator chains

One method, next, returning Some(item) until None.

Section

Adapters

map, filter, enumerate, zip, take, skip, chain, flat_map.

Section

Consumers

sum, count, find, any, all, fold, min_by_key. Nothing happens until one runs.

Formal. Methods like map, filter, zip and take return a new iterator and do no work when they are called. Work happens only when something calls next: a for loop or a finishing method (collect, sum, count, fold, find, ...). Each call to next pulls one item through the whole chain, and a finishing method that has its answer (find, any, or a take that is full) stops pulling. A chain that is never finished triggers the unused_must_use warning.

In plain English. Writing .map(...) is writing down a recipe, not cooking. Nothing is cooked until someone asks for a plate, and each plate goes through every step before the next one starts.

Example. (1..).map(|n| n * n).filter(|n| n % 2 == 1).take(3) is fine even though 1.. never ends: collect pulls exactly the items needed for 1, 9 and 25, then stops.

Why this rule exists. Laziness lets a chain skip work it doesn't need, avoid building intermediate collections, and describe infinite sequences, while compiling to the same code as a hand-written loop.

A chain with no consumer does nothing, and the compiler warns you.

One collect call builds a Vec, a HashMap, a String, or a Result of a Vec.

Your own sequence

A struct with next, and every adapter comes free.

The trait that turns a value into an iterator; for loops call it.

Example

Implementing IntoIterator for &Playlist is what lets a caller write for song in &playlist.
An iterator-like type whose items borrow from the iterator itself, so only one can be held at a time.

The one shape Iterator can't express, and the generic associated types that do.

Choosing, and going faster

Choose by what reads clearly. Measured, they cost the same.

The same work split across threads by hand, then with rayon's par_iter.