With iterators, you can work through a collection without managing an index or checking its bounds yourself. You build a pipeline of operations, but they're lazy: the work waits until you ask for a result. With optimizations enabled, the compiler can often turn chained iterator calls into a loop comparable to one you would write by hand.
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.
You can write the chain first, then add a type annotation on the binding if the
compiler needs one.
.collect() 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)
}
You no longer have to maintain the mut counters or keep track of the maximum
yourself.
A running total is a good first place to see what an iterator consumer does. Rust's iterators are lazy, so they don't do any work until you ask for a result.
You could add the values with a for loop and an accumulator. Here, sum asks
the iterator for each value and collapses the sequence into one total.
Useful from the Standard Library
<[T]>::iterproduces an iterator of shared references over the slice.Iterator::sumreduces a numeric iterator to a single total. The function'si32return type supplies its output type when you returnsales.iter().sum()directly. In other contexts, you may need an annotation (let total: i32 = ...) or the turbofish (.sum::<i32>()).
sales.iter().sales.iter().sum(). When you return this expression directly, the
function's i32 return type tells sum which type to produce.
/// 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. Read this pipeline from left to right: take ownership of the vector's items, transform each one, then collect the results into a new vector.
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. The input vector is no longer needed after this call.to_lowercaseonly borrows each string, so ownership is not required for the conversion itself.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 characters converted to lowercase. Lowercasing is not the same as Unicode case folding for caseless comparison.
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"]);
}
With map you transformed every item, while filter keeps some items and drops
the rest. There is one borrowing detail to watch: usernames.into_iter() yields
&str, and filter gives its closure a reference to each item, so the closure
sees &&str.
Method calls such as s.starts_with(...) automatically dereference these
layers. The extra references can be hard to track at first. If the compiler
reports a missing &, check what the iterator yields and what the closure
receives. The iterators entry in the cheatsheet shows those
reference layers side by side.
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. You don't need a turbofish here.
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') works without an explicit
dereference.
/// 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"]);
}
This time the input is a &[&str], a borrowed slice of borrowed strings, so the
iterator yields &&str. You'll return owned Strings so the caller can keep
the results independently of the input. That lets us focus on the iterator chain
without adding lifetime annotations.
str::to_string
converts each surviving &&str into an owned String through auto-deref. Chain
it after your filter with a map, then collect into a Vec.
Useful from the Standard Library
Iterator::filterpasses&&&strto the predicate here: a reference to the iterator's&&stritem. Method-call auto-deref lets you call.ends_with(".rs")directly.Iterator::mapapplies your conversion closure to each surviving&&str.str::to_stringconverts a borrowed string slice into an ownedString. Auto-deref reaches through the extra reference for you.str::ends_withis the suffix check used by the predicate.
&&&str. Method-call
auto-deref also works 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"]);
}
In the ? chapter, you parsed each token inside a for loop. Write
sum_numbers again, this time with an iterator pipeline and sum, without a
loop or ?. Keep the same behavior: whitespace separates integers, empty input
returns Ok(0), and the first parse error ends the sum. Assume the running
total fits in an i32.
Iterator::sum
also works on an iterator of Result values. Its output can be a Result
containing either the total or the first error. The function's return type can
tell it which output type to use.
Compare the two versions after the tests pass. In the loop, ? returns from
sum_numbers when parsing fails. Here, sum stops asking for items and returns
an Err to its caller; it does not return from the surrounding function. Your
function can inspect that result or return it directly. Adding ? after sum
would propagate an error that sum has already found.
Result values as the
iterator's items.map can apply parsing to each token. The return type tells sum to produce
Result<i32, ParseIntError>.
/// Sums whitespace-separated integers with an iterator pipeline and fallible
/// `sum`.
fn sum_numbers(text: &str) -> Result<i32, std::num::ParseIntError> {
text.split_whitespace()
.map(|token| token.parse::<i32>())
.sum()
}
#[test]
fn test_sum_numbers() {
assert_eq!(sum_numbers("5\n10\n15").unwrap(), 30);
assert_eq!(sum_numbers(" 1 2 3 ").unwrap(), 6);
assert!(sum_numbers("5\nabc\n15").is_err()); // not a number
}
#[test]
fn test_sum_numbers_empty_and_signed() {
assert_eq!(sum_numbers(""), Ok(0));
assert_eq!(sum_numbers(" \t\n"), Ok(0));
assert_eq!(sum_numbers("-5 +2 3"), Ok(0));
}
#[test]
fn test_sum_numbers_first_error() {
let first_error = "abc".parse::<i32>().unwrap_err();
assert_eq!(sum_numbers("5 abc 99999999999999999999"), Err(first_error));
}
The tests below already contain the pipelines. Before running them, replace each
todo!() with your prediction; leave the pipelines unchanged.
For the first test, predict how many times the closure runs when nothing
consumes the iterator. For the second, predict the first result, the collected
batch, the next result, and the inputs visited by map. Write down the order of
the printed messages too. Then run the tests and compare the trace with your
predictions. Locally, use
cargo test --example 17_iterators _8_lazy_consumption:: -- --nocapture to see
output from passing tests.
next
asks for one item and returns an Option.by_ref
lets a consumer borrow an iterator so you can use it again afterward.take
limits how many items its consumer can request.Does asking for one filtered result visit just one input? After collecting the
batch, does the next call start over or resume? Try changing take(1) to
take(2), predict all the results again, and rerun.
map adapter doesn't call its closure. Something must ask for an
item.filter may request several inputs before it can yield one output.by_ref borrows the existing iterator; consuming that borrow advances the
original too.
#[test]
fn experiment_without_consumption() {
let mut calls = 0;
{
let _pipeline = (1..=8).map(|number| {
calls += 1;
number * 10
});
println!("pipeline constructed");
}
println!("closure calls: {calls}");
let expected_calls: usize = 0;
assert_eq!(calls, expected_calls);
}
#[test]
fn experiment_partial_consumption() {
let mut visited = Vec::new();
let (first, batch, next) = {
let mut pipeline = (1..=8)
.map(|number| {
visited.push(number);
println!("map visits {number}");
number * 10
})
.filter(|number| number % 20 == 0);
println!("pipeline constructed");
let first = pipeline.next();
println!("first: {first:?}");
let batch: Vec<_> = pipeline.by_ref().take(1).collect();
println!("batch: {batch:?}");
let next = pipeline.next();
println!("next: {next:?}");
(first, batch, next)
};
println!("visited: {visited:?}");
let expected_first: Option<i32> = Some(20);
let expected_batch: Vec<i32> = vec![40];
let expected_next: Option<i32> = Some(60);
let expected_visited: Vec<i32> = vec![1, 2, 3, 4, 5, 6];
assert_eq!(first, expected_first);
assert_eq!(batch, expected_batch);
assert_eq!(next, expected_next);
assert_eq!(visited, expected_visited);
}
You summed a numeric array 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 pipeline starts with
.iter(),.iter_mut(),.into_iter(), or a method such as.chars()or.lines(). Lazy adapters describe what should happen to each item. A consumer finishes the pipeline by asking for results.
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.
sumneeds to know its output type. The function's return type can supply it; otherwise annotate the binding or use.sum::<i32>().
filter's closure always takes&T, so on a&striterator you'll see&&str. Method calls auto-deref, so.starts_with(...)works through extra references; comparison operators sometimes need an explicit*.The
|x| ...syntax you've been seeing is a closure: an anonymous function passed as an argument.Fallible
sumstops at the firstErrand returns it as a value. Unlike?, it doesn't return from the surrounding function.
nextandtakecan consume only part of a pipeline. Withby_ref, you can resume the same iterator afterward; already-consumed items don't run again.
Extra practice to explore at your own pace. These chapters do not count toward course progress.