Rust iterators are the best thing since &[bread].
Iterators turn "loop over a collection and do something" into a pipeline. They're lazy: nothing actually runs until you ask for a result. The compiler usually fuses chained iterator calls into a single tight loop, so the abstraction is free at runtime.
Here's how iterators work in practice:
.iter(), .into_iter(), .iter_mut(), or
directly from .chars(), .lines(), etc..map(...), .filter(...), .take(...). These
are lazy..collect(), .sum(), .count(),
.any(...), or a for loop.let names = vec!["alice", "ADMIN", "bob"];
let active: Vec<String> = names
.iter() // &&str
.filter(|n| n.starts_with('a')) // keep some
.map(|n| n.to_lowercase()) // transform
.collect(); // back to Vec<String>
// active == ["alice"]
The three "iter" methods differ in what they yield:
.iter() yields &T (immutable references). Use when reading..iter_mut() yields &mut T. Use when modifying in place..into_iter() yields T (consumes the collection). Use when you don't
need the original anymore.Some adapters change the item type. After .map(|n| n.to_lowercase()),
the items are Strings, not &&strs. The compiler infers types through
the chain, so trust it: write the chain, then add a type annotation on
the binding if needed.
.collect() is interesting: it can produce many different collections.
Tell it which one with a type annotation: Vec<_>, HashMap<_, _>,
String. The _ lets the compiler fill in the inner types.
Remember the three little functions from chapter 5's exercise break?
Each one was a counter, a for loop, and a return. With iterators,
the whole trio shrinks to:
fn word_count(text: &str) -> usize { text.split_whitespace().count() }
fn char_count(text: &str) -> usize { text.chars().count() }
fn longest_word(text: &str) -> usize {
text.split_whitespace().map(|w| w.chars().count()).max().unwrap_or(0)
}
The loops, the mut counters, the running-maximum bookkeeping — all
gone. That's the payoff for spending a chapter on iterators: every
"walk a collection and reduce it to one number, or to one new
collection" problem gets shorter and harder to get wrong.
Iterators were popularised by functional languages like Lisp (created by John McCarthy in 1958), and today they're a core building block in most modern languages. Rust's iterators are lazy: they don't do any work until you ask for a result.
The simplest pattern is to take a sequence and collapse it down to a
single value. You could write a for loop with a running total, but
the standard library can do this for you in one call.
Useful from the standard library
<[T]>::iterproduces an iterator of shared references over the slice.Iterator::sumreduces a numeric iterator to a single total. It's generic over the output type, so the compiler needs a hint: either annotate the binding (let total: i32 = ...) or use the turbofish (.sum::<i32>()).Iterator::productis the multiplicative cousin if you ever need a running product.
sales.iter().sales.iter().sum(). And you'll need a type annotation
(let total: i32 = …, or .sum::<i32>()) because sum is generic.Now you need to transform every element instead of collapsing the
sequence. The pattern is vec.into_iter() -> some combinator that
applies a closure -> back to a Vec via collect().
map is lazy: it just describes the transformation. Nothing runs
until collect (or another consumer) asks for the results.
Useful from the standard library
Vec::into_iterconsumes the vec and yields owned items. That's what lets the closure call.to_lowercase()on aStringdirectly.Iterator::mapapplies a closure to each item and produces a new iterator with the transformed items.Iterator::collectturns the pipeline back into a collection. The return type (Vec<String>) tellscollectwhich collection to produce.str::to_lowercasereturns a freshStringwith the case folded.
into_iter() (consume the input vec) → map(...) → collect().String. Call .to_lowercase() on it.emails.into_iter().map(|e| e.to_lowercase()).collect()
Same idea as map, but instead of transforming each element you keep
some and drop others. Watch out for one borrowing gotcha: .iter()
yields &T, but filter gives its closure another reference on top,
so the closure sees &&T.
That's why you'll often see **c == ... or s.starts_with(...) (which
auto-derefs) instead of plain c == .... Don't be alarmed when the
compiler complains about a missing &, see
the cheatsheet entry on iterators.
Useful from the standard library
Iterator::filterkeeps only items where the predicate returnstrue. The closure receives a reference to the item, regardless of whether the iterator yields owned values or borrows.str::starts_withtakes achar(or another&str) and answers yes/no. Method-call syntax auto-derefs through the extra reference.collect()here picksVec<&str>straight from the return type. No turbofish needed.
into_iter() → filter(...) → collect().filter's closure takes a reference to each item. Since
the iterator yields &str, the closure parameter is &&str. Method
calls auto-deref, so |s| s.starts_with('a') Just Works.usernames.into_iter().filter(|s| s.starts_with('a')).collect()
Same as the previous step, but the input is a &[&str] (a
borrowed slice of borrowed strings), so the iterator yields &&str.
We sidestep that double-reference by returning owned Strings; the
lesson here is iterators, not lifetimes.
To go from &&str to String, reach for [str::to_string]. Chain
it after your filter with a map, then collect into a Vec.
Useful from the standard library
Iterator::filteragain. Same closure structure; auto-deref still saves you for.ends_with(".rs").Iterator::mapis what converts the surviving&&strs into ownedStrings.str::to_stringis the easy&str->Stringcall. Auto-deref reaches through the extra reference for you.str::ends_withis the suffix check used by the predicate.
&&&str.
Auto-deref still saves you for .ends_with(".rs").Vec<String>, not Vec<&str>. Add a .map(...)
step that converts each &&str into an owned String.files
.iter()
.filter(|name| name.ends_with(".rs"))
.map(|name| name.to_string())
.collect()
You collapsed a numeric vector with sum, transformed every element
with map, kept just the matching ones with filter, and combined
filter with map to convert borrowed slices into owned strings.
What we learned
- An iterator is a pipeline: get one with
.iter()/.iter_mut()/.into_iter()(or directly from things like.chars()and.lines()), chain lazy adapters, then finish with a consumer.iteryields&T,iter_mutyields&mut T,into_itermoves out of the collection and yieldsT. Pick the one that matches what you intend to do with each item.- Adapters (
map,filter,take,skip, ...) describe the pipeline but do nothing on their own. The actual work happens when a consumer (collect,sum,count,forloop) asks for results.collectis generic over the target collection. The return type (or a turbofish like.collect::<Vec<_>>()) tells it what to build.sumandproductneed to know their output type. Annotate the binding or use.sum::<i32>()to keep the compiler happy.filter's closure always takes&T, so on a&striterator you'll see&&str. Method calls auto-deref, so.starts_with(...)Just Works; comparison operators sometimes need an explicit*.- The
|x| ...syntax you've been seeing is a closure: an anonymous function passed as an argument. Iterators are where closures earn their keep.