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 the word count chapter'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.
/// Calculates total revenue from sales data.
///
/// The simplest iterator pattern: take a sequence, produce one number.
/// You could write a `for` loop with a running total, but the standard
/// library can collapse a numeric iterator down for you in one call.
/// See: <https://doc.rust-lang.org/std/iter/trait.Iterator.html>
fn calculate_total_revenue() -> i32 {
let sales = [1200, 850, 2300, 950, 1800, 3200, 1100, 2800];
sales.iter().sum()
}
#[test]
fn test_calculate_total_revenue() {
let total = calculate_total_revenue();
assert_eq!(total, 14200); // Sum of all sales
}
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.
/// Normalizes email addresses to lowercase.
///
/// 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()`.
/// See: <https://doc.rust-lang.org/std/string/struct.String.html#method.to_lowercase>
fn normalize_emails(emails: Vec<String>) -> Vec<String> {
emails
.into_iter()
.map(|email| email.to_lowercase())
.collect()
}
#[test]
fn test_normalize_emails() {
let emails = vec!["Alice@EXAMPLE.COM".to_string(), "BOB@test.ORG".to_string()];
let normalized = normalize_emails(emails);
assert_eq!(normalized, vec!["alice@example.com", "bob@test.org"]);
}
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.
/// Returns all users whose usernames start with 'a'.
///
/// Same idea, but instead of transforming each element you keep some
/// and drop others. Watch out for one borrowing gotcha: the closure
/// receives a reference to each element, not the element itself.
/// See: <https://doc.rust-lang.org/std/primitive.str.html#method.starts_with>
fn select_usernames_starting_with_a(usernames: Vec<&str>) -> Vec<&str> {
usernames
.into_iter()
.filter(|username| username.starts_with('a'))
.collect()
}
#[test]
fn test_select_usernames_starting_with_a() {
let users = vec!["alice", "admin", "bob", "anonymous", "charlie"];
let active = select_usernames_starting_with_a(users);
assert_eq!(active, vec!["alice", "admin", "anonymous"]);
}
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.
/// Finds all files with ".rs" extension.
///
/// Same idea as the previous one, 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 `String`s; the
/// lesson here is iterators, not lifetimes. To go from `&&str` to
/// `String`, reach for [`str::to_string`].
fn find_rust_files(files: &[&str]) -> Vec<String> {
files
.iter()
.filter(|file| file.ends_with(".rs"))
.map(|file| file.to_string())
.collect()
}
#[test]
fn test_find_rust_files() {
let files = &[
"main.rs",
"README.md",
"lib.rs",
"package.json",
"config.rs",
];
let rust_files = find_rust_files(files);
assert_eq!(rust_files, vec!["main.rs", "lib.rs", "config.rs"]);
}
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.