Lesson 39 of 94
Iterator Adapters: The Complete Cookbook
This lesson is a cookbook: one adapter or consumer per section, each with a small realistic example. Skim the table, then jump to what you need.
| Rust | Section |
|---|---|
map |
1 |
filter |
2 |
filter_map |
3 |
flat_map |
4 |
flatten |
5 |
enumerate |
6 |
zip |
7 |
chain |
8 |
take, skip |
9 |
take_while, skip_while |
10 |
step_by |
11 |
rev |
12 |
fold |
13 |
reduce |
14 |
scan |
15 |
sum, product, count |
16 |
min, max, min_by_key, max_by_key, max_by |
17 |
find, find_map |
18 |
position, rposition |
19 |
any, all |
20 |
partition |
21 |
unzip |
22 |
inspect |
23 |
peekable |
24 |
cycle |
25 |
last, nth |
26 |
chunk_by (on slices) |
27 |
dedup (on Vec) |
28 |
sorted via collect + sort |
29 |
collect to String, join |
30 |
1. map: transform every item
fn main() {
let celsius = [0.0, 21.5, 37.0, 100.0];
let fahrenheit: Vec<f64> = celsius.iter().map(|c| c * 9.0 / 5.0 + 32.0).collect();
println!("{:?}", fahrenheit);
}[32.0, 70.7, 98.6, 212.0]2. filter: keep matching items
The closure receives a reference to each item, so with iter() over numbers you get &&i32. Patterns like |&&x| or |x| **x > ... handle that; so does comparing references.
fn main() {
let latencies_ms = [120, 950, 85, 1_400, 300];
let slow: Vec<u32> = latencies_ms.iter().copied().filter(|&ms| ms > 500).collect();
let slow_refs: Vec<&u32> = latencies_ms.iter().filter(|ms| **ms > 500).collect();
println!("{:?} {:?}", slow, slow_refs);
}[950, 1400] [950, 1400]3. filter_map: transform and drop failures in one step
The closure returns an Option: Some(value) keeps the transformed value, None drops the item. It's the idiomatic way to parse and skip invalid entries.
fn main() {
let raw = ["42", " 7 ", "abc", "", "100"];
let numbers: Vec<i32> = raw.iter().filter_map(|s| s.trim().parse().ok()).collect();
println!("{:?}", numbers);
let emails = ["ana@example.com", "not-an-email", "ben@corp.com"];
let domains: Vec<&str> = emails.iter().filter_map(|e| e.split_once('@').map(|(_, d)| d)).collect();
println!("{:?}", domains);
}[42, 7, 100]
["example.com", "corp.com"]4. flat_map: one item to many
fn main() {
let orders = [("ord-1", vec!["book", "pen"]), ("ord-2", vec![]), ("ord-3", vec!["lamp"])];
let all_items: Vec<&str> = orders.iter().flat_map(|(_, items)| items.iter().copied()).collect();
println!("{:?}", all_items);
let sentences = ["Rust is fast", "Iterators compose"];
let words: Vec<&str> = sentences.iter().flat_map(|s| s.split(' ')).collect();
println!("{:?}", words);
}["book", "pen", "lamp"]
["Rust", "is", "fast", "Iterators", "compose"]5. flatten: remove one level of nesting
flatten works on an iterator of iterables: a Vec<Vec<T>>, or an iterator of Options (dropping the Nones).
fn main() {
let batches = vec![vec![1, 2], vec![], vec![3, 4, 5]];
let all: Vec<i32> = batches.into_iter().flatten().collect();
println!("{:?}", all);
let maybe_ids = [Some(7), None, Some(9), None];
let ids: Vec<i32> = maybe_ids.iter().flatten().copied().collect();
println!("{:?}", ids);
}[1, 2, 3, 4, 5]
[7, 9]6. enumerate: pair each item with its index
fn main() {
let lines = ["fn main() {", " println!(\"hi\")", "}"];
for (n, line) in lines.iter().enumerate() {
println!("{:>3} | {line}", n + 1);
}
let first_long = lines.iter().enumerate().find(|(_, l)| l.len() > 12).map(|(i, _)| i + 1);
println!("first long line: {:?}", first_long);
} 1 | fn main() {
2 | println!("hi")
3 | }
first long line: Some(2)7. zip: walk two iterators in step
zip pairs items from two iterators and stops at the shorter one.
fn main() {
let names = ["cpu", "memory", "disk"];
let values = [91.5, 62.0, 78.25];
let limits = [90.0, 80.0, 95.0];
for ((name, value), limit) in names.iter().zip(values).zip(limits) {
let status = if value > limit { "ALERT" } else { "ok" };
println!("{name:<7} {value:>6.2} / {limit:<5} {status}");
}
let a = [1, 2, 3];
let b = [10, 20, 30];
let dot: i32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
println!("dot product {dot}");
}cpu 91.50 / 90 ALERT
memory 62.00 / 80 ok
disk 78.25 / 95 ok
dot product 1408. chain: one sequence after another
fn main() {
let defaults = vec!["--color", "--quiet"];
let user_args = vec!["build", "--release"];
let all: Vec<&str> = defaults.iter().chain(user_args.iter()).copied().collect();
println!("{:?}", all);
let header = std::iter::once("id,name");
let rows = ["1,ana", "2,ben"].into_iter();
let csv: Vec<&str> = header.chain(rows).collect();
println!("{}", csv.join("\n"));
}["--color", "--quiet", "build", "--release"]
id,name
1,ana
2,benstd::iter::once(x) is an iterator that yields a single item.
9. take and skip: pagination
fn main() {
let results: Vec<u32> = (1..=23).collect();
let page_size = 10;
for page in 0..3 {
let items: Vec<u32> = results.iter().skip(page * page_size).take(page_size).copied().collect();
println!("page {page}: {:?}", items);
}
}page 0: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
page 1: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 2: [21, 22, 23]10. take_while and skip_while
These stop or start based on a condition rather than a count. They are ideal for sorted data and headers.
fn main() {
let sorted_timestamps = [100, 250, 400, 900, 1_300, 2_000];
let before_1000: Vec<u32> = sorted_timestamps.iter().copied().take_while(|&t| t < 1_000).collect();
let from_1000: Vec<u32> = sorted_timestamps.iter().copied().skip_while(|&t| t < 1_000).collect();
println!("{:?} {:?}", before_1000, from_1000);
let file = "# comment\n# another\nkey=value\n# not skipped\nother=1";
let body: Vec<&str> = file.lines().skip_while(|l| l.starts_with('#')).collect();
println!("{:?}", body);
}[100, 250, 400, 900] [1300, 2000]
["key=value", "# not skipped", "other=1"]11. step_by: every nth item
fn main() {
let samples: Vec<u32> = (0..20).map(|i| i * 3).collect();
let downsampled: Vec<u32> = samples.iter().step_by(5).copied().collect();
println!("{:?}", downsampled);
let odd_positions: Vec<char> = "abcdefg".chars().skip(1).step_by(2).collect();
println!("{:?}", odd_positions);
}[0, 15, 30, 45]
['b', 'd', 'f']12. rev: iterate backwards
Most iterators over collections and ranges can run backwards, because they implement DoubleEndedIterator.
fn main() {
let history = ["/home", "/products", "/products/42", "/cart"];
let recent_first: Vec<&str> = history.iter().rev().copied().collect();
println!("{:?}", recent_first);
let last_two: Vec<&&str> = history.iter().rev().take(2).collect();
println!("{:?}", last_two);
let countdown: String = (1..=5).rev().map(|n| n.to_string()).collect::<Vec<_>>().join(" ");
println!("{countdown}");
}["/cart", "/products/42", "/products", "/home"]
["/cart", "/products/42"]
5 4 3 2 113. fold: combine everything into one value
fold(initial, |accumulator, item| ...) is the general way to reduce a sequence. sum, count, and max are all special cases of it.
fn main() {
let transactions = [("deposit", 500), ("withdraw", 120), ("deposit", 75), ("withdraw", 900)];
let (balance, rejected) = transactions.iter().fold((0_i64, 0), |(bal, rej), (kind, amount)| match *kind {
"deposit" => (bal + amount, rej),
_ if *amount <= bal => (bal - amount, rej),
_ => (bal, rej + 1),
});
println!("balance {balance}, rejected {rejected}");
let csv_line = ["a", "b", "c"].iter().fold(String::new(), |acc, s| if acc.is_empty() { s.to_string() } else { acc + "," + s });
println!("{csv_line}");
}balance 455, rejected 1
a,b,c14. reduce: fold without an initial value
reduce uses the first item as the starting value, so it returns an Option (the iterator might be empty).
fn main() {
let durations = [30, 45, 15];
let total = durations.iter().copied().reduce(|a, b| a + b);
let longest_word = ["iter", "reduce", "fold"].into_iter().reduce(|a, b| if b.len() > a.len() { b } else { a });
let empty: [u32; 0] = [];
println!("{:?} {:?} {:?}", total, longest_word, empty.iter().copied().reduce(|a, b| a + b));
}Some(90) Some("reduce") None15. scan: running values
scan is like fold, but yields every intermediate state. It's how you compute running totals and balances.
fn main() {
let daily_changes = [100, -30, 45, -200, 60];
let balances: Vec<i32> = daily_changes
.iter()
.scan(1_000, |balance, change| {
*balance += change;
Some(*balance)
})
.collect();
println!("{:?}", balances);
let readings = [3.0, 5.0, 4.0, 8.0];
let running_avg: Vec<f64> = readings
.iter()
.scan((0.0, 0), |(sum, n), &x| {
*sum += x;
*n += 1;
Some(*sum / *n as f64)
})
.collect();
println!("{:?}", running_avg);
}[1100, 1070, 1115, 915, 975]
[3.0, 4.0, 4.0, 5.0]16. sum, product, and count
fn main() {
let cart = [(2, 1_250), (1, 4_999), (3, 199)]; // (quantity, unit price in cents)
let items: u32 = cart.iter().map(|(q, _)| q).sum();
let total_cents: u32 = cart.iter().map(|(q, p)| q * p).sum();
let lines = cart.iter().count();
let combinations: u64 = (1..=4).product();
println!("{lines} lines, {items} items, total {:.2}, {combinations} orderings", total_cents as f64 / 100.0);
}3 lines, 6 items, total 80.96, 24 orderings17. min, max, and their _by_key and _by variants
max_by_key picks the item with the largest key. max_by takes a comparison closure, which is needed for floats.
fn main() {
let employees: [(&str, u32, f64); 3] = [("ana", 34, 9_000.0), ("ben", 51, 7_200.0), ("cy", 28, 9_800.0)];
let oldest = employees.iter().max_by_key(|(_, age, _)| *age);
let youngest = employees.iter().min_by_key(|(_, age, _)| *age);
let top_paid = employees.iter().max_by(|a, b| a.2.total_cmp(&b.2));
println!("{:?}\n{:?}\n{:?}", oldest, youngest, top_paid);
let longest_name = employees.iter().map(|(n, _, _)| n.len()).max();
println!("{:?}", longest_name);
}Some(("ben", 51, 7200.0))
Some(("cy", 28, 9800.0))
Some(("cy", 28, 9800.0))
Some(3)When two items tie, max_by_key returns the last one and min_by_key returns the first one.
18. find and find_map
find returns the first matching item. find_map is filter_map followed by next: it returns the first successful transformation.
fn main() {
let servers = [("web-1", false), ("web-2", true), ("web-3", true)];
let first_healthy = servers.iter().find(|(_, healthy)| *healthy).map(|(name, _)| *name);
println!("{:?}", first_healthy);
let config_lines = ["# comment", "timeout = abc", "port = 8080", "port = 9090"];
let port: Option<u16> = config_lines
.iter()
.find_map(|line| line.strip_prefix("port = ").and_then(|v| v.parse().ok()));
println!("{:?}", port);
}Some("web-2")
Some(8080)19. position and rposition
fn main() {
let log = ["INFO boot", "WARN low disk", "ERROR crash", "INFO restart", "ERROR crash again"];
let first_error = log.iter().position(|l| l.starts_with("ERROR"));
let last_error = log.iter().rposition(|l| l.starts_with("ERROR"));
println!("{:?} {:?}", first_error, last_error);
if let Some(i) = first_error {
println!("context before first error: {:?}", &log[..i]);
}
}Some(2) Some(4)
context before first error: ["INFO boot", "WARN low disk"]20. any and all
fn main() {
let password = "Tr0ub4dor&3";
let checks = [
("length >= 10", password.len() >= 10),
("has digit", password.chars().any(|c| c.is_ascii_digit())),
("has symbol", password.chars().any(|c| !c.is_alphanumeric())),
("all printable", password.chars().all(|c| !c.is_control())),
];
for (name, ok) in checks {
println!("{name:<14} {ok}");
}
println!("strong: {}", checks.iter().all(|(_, ok)| *ok));
}length >= 10 true
has digit true
has symbol true
all printable true
strong: true21. partition: split into two collections
fn main() {
let files = ["main.rs", "README.md", "lib.rs", "Cargo.toml", "utils.rs"];
let (rust, other): (Vec<&str>, Vec<&str>) = files.iter().partition(|f| f.ends_with(".rs"));
println!("rust: {:?}\nother: {:?}", rust, other);
}rust: ["main.rs", "lib.rs", "utils.rs"]
other: ["README.md", "Cargo.toml"]22. unzip: split pairs into two collections
fn main() {
let points = [(1.0, 2.0), (3.5, -1.0), (0.0, 4.25)];
let (xs, ys): (Vec<f64>, Vec<f64>) = points.iter().copied().unzip();
println!("{:?} {:?}", xs, ys);
let (names, scores): (Vec<&str>, Vec<u32>) = [("ana", 91), ("ben", 78)].into_iter().unzip();
println!("{:?} {:?}", names, scores);
}[1.0, 3.5, 0.0] [2.0, -1.0, 4.25]
["ana", "ben"] [91, 78]23. inspect: look at items without changing them
inspect runs a closure on each item as it passes, which is handy for debugging a pipeline.
fn main() {
let total: i32 = [4, -2, 7, -5, 3]
.iter()
.inspect(|x| println!("in: {x}"))
.filter(|&&x| x > 0)
.inspect(|x| println!(" kept: {x}"))
.sum();
println!("total {total}");
}in: 4
kept: 4
in: -2
in: 7
kept: 7
in: -5
in: 3
kept: 3
total 1424. peekable: look ahead without consuming
peekable() adds a peek() method that shows the next item without taking it. Parsers use it constantly.
fn main() {
let input = "aaabccdddd";
let mut chars = input.chars().peekable();
let mut encoded = String::new();
while let Some(c) = chars.next() {
let mut run = 1;
while chars.peek() == Some(&c) {
chars.next();
run += 1;
}
encoded.push_str(&format!("{run}{c}"));
}
println!("{input} -> {encoded}");
}aaabccdddd -> 3a1b2c4dThis is run-length encoding. next_if shortens the inner loop further: while chars.next_if_eq(&c).is_some() { run += 1; }.
25. cycle: repeat forever
cycle repeats an iterator endlessly. Pair it with zip or take so it ends.
fn main() {
let workers = ["w1", "w2", "w3"];
let jobs = ["resize", "encode", "thumbnail", "upload", "notify"];
for (job, worker) in jobs.iter().zip(workers.iter().cycle()) {
println!("{job:<10} -> {worker}");
}
let stripes: String = ['#', '.'].iter().cycle().take(9).collect();
println!("{stripes}");
}resize -> w1
encode -> w2
thumbnail -> w3
upload -> w1
notify -> w2
#.#.#.#.#This round-robin assignment is a common load-balancing strategy.
26. last and nth
fn main() {
let versions = ["1.0.0", "1.1.0", "1.2.3", "2.0.0-beta"];
println!("{:?}", versions.iter().last());
println!("{:?}", versions.iter().nth(1));
println!("{:?}", versions.iter().nth(10));
let mut it = versions.iter();
it.nth(1); // consumes items 0 and 1
println!("after nth(1): {:?}", it.next());
}Some("2.0.0-beta")
Some("1.1.0")
None
after nth(1): Some("1.2.3")27. chunk_by: group consecutive items
Slices have chunk_by, which groups consecutive items for which a closure returns true. It's perfect for already-sorted data or runs in time series.
fn main() {
let log_levels = ["INFO", "INFO", "WARN", "ERROR", "ERROR", "ERROR", "INFO"];
let runs: Vec<(&str, usize)> = log_levels.chunk_by(|a, b| a == b).map(|run| (run[0], run.len())).collect();
println!("{:?}", runs);
let temps = [18, 19, 21, 25, 26, 24, 17, 16];
let rising: Vec<&[i32]> = temps.chunk_by(|a, b| b > a).collect();
println!("{:?}", rising);
}[("INFO", 2), ("WARN", 1), ("ERROR", 3), ("INFO", 1)]
[[18, 19, 21, 25, 26], [24], [17], [16]]For grouping all items by a key regardless of order, use a HashMap (HashMap), the equivalent of Collectors.groupingBy.
28. Removing duplicates
Iterators have no distinct. The idiomatic options: sort + dedup on a Vec when order doesn't matter, or a HashSet to keep first occurrences in order.
use std::collections::HashSet;
fn main() {
let tags = vec!["rust", "java", "rust", "go", "java", "zig"];
let mut sorted_unique = tags.clone();
sorted_unique.sort();
sorted_unique.dedup();
println!("{:?}", sorted_unique);
let mut seen = HashSet::new();
let first_seen_order: Vec<&str> = tags.iter().copied().filter(|t| seen.insert(*t)).collect();
println!("{:?}", first_seen_order);
}["go", "java", "rust", "zig"]
["rust", "java", "go", "zig"]HashSet::insert returns true only the first time a value is added, which makes it a perfect filter predicate.
29. Sorting inside a pipeline
Iterators can't be sorted, because sorting needs all items at once. Collect first, then sort. Sorting by a derived key uses sort_by_key, or sort_by_cached_key when the key is expensive.
fn main() {
let files = ["report_10.pdf", "report_2.pdf", "report_1.pdf", "summary.pdf"];
let mut by_number: Vec<&str> = files.iter().copied().filter(|f| f.starts_with("report_")).collect();
by_number.sort_by_key(|f| f.trim_start_matches("report_").trim_end_matches(".pdf").parse::<u32>().unwrap_or(0));
println!("{:?}", by_number);
let mut words = vec!["banana", "Apple", "cherry"];
words.sort_by_cached_key(|w| w.to_lowercase());
println!("{:?}", words);
}["report_1.pdf", "report_2.pdf", "report_10.pdf"]
["Apple", "banana", "cherry"]30. Joining into a String
fn main() {
let tags = ["rust", "iterators", "cookbook"];
println!("{}", tags.join(", "));
let hashtags: String = tags.iter().map(|t| format!("#{t}")).collect::<Vec<_>>().join(" ");
println!("{hashtags}");
let sql_list = (1..=4).map(|id| id.to_string()).collect::<Vec<_>>().join(",");
println!("WHERE id IN ({sql_list})");
}rust, iterators, cookbook
#rust #iterators #cookbook
WHERE id IN (1,2,3,4)31. Realistic example: an access-log report
Several adapters combined to analyze web server logs.
fn main() {
let log = "\
GET /api/users 200 45
GET /api/users 200 51
POST /api/orders 201 180
GET /api/orders/9 404 12
GET /static/app.js 200 3
POST /api/orders 500 950
GET /api/users 200 48";
let entries: Vec<(&str, &str, u16, u32)> = log
.lines()
.filter_map(|line| {
let mut p = line.split_whitespace();
Some((p.next()?, p.next()?, p.next()?.parse().ok()?, p.next()?.parse().ok()?))
})
.collect();
let api_calls = entries.iter().filter(|e| e.1.starts_with("/api")).count();
let errors: Vec<String> = entries.iter().filter(|e| e.2 >= 400).map(|e| format!("{} {} -> {}", e.0, e.1, e.2)).collect();
let avg_ms = entries.iter().map(|e| e.3 as f64).sum::<f64>() / entries.len() as f64;
let slowest = entries.iter().max_by_key(|e| e.3).map(|e| (e.1, e.3));
let error_rate = errors.len() as f64 * 100.0 / entries.len() as f64;
println!("{} requests, {api_calls} to the API", entries.len());
println!("average {avg_ms:.1} ms, slowest {:?}", slowest);
println!("error rate {error_rate:.1}%: {:?}", errors);
}7 requests, 6 to the API
average 184.1 ms, slowest Some(("/api/orders", 950))
error rate 28.6%: ["GET /api/orders/9 -> 404", "POST /api/orders -> 500"]32. Realistic example: a leaderboard with ranks and ties
fn main() {
let mut scores = vec![("ana", 980), ("ben", 1_200), ("cy", 980), ("dee", 750), ("eve", 1_200)];
scores.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
let ranked: Vec<(usize, &str, u32)> = scores
.iter()
.enumerate()
.scan((0, None), |(rank, prev_score), (i, (name, score))| {
if *prev_score != Some(*score) {
*rank = i + 1; // ties share a rank
*prev_score = Some(*score);
}
Some((*rank, *name, *score))
})
.collect();
for (rank, name, score) in &ranked {
println!("#{rank} {name:<4} {score}");
}
}#1 ben 1200
#1 eve 1200
#3 ana 980
#3 cy 980
#5 dee 75033. Realistic example: batch processing with chunks and totals
fn main() {
let user_ids: Vec<u32> = (1..=11).collect();
let batch_size = 4;
let (sent, batches) = user_ids
.chunks(batch_size)
.enumerate()
.map(|(i, batch)| {
let ids = batch.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
println!("batch {}: POST /notify ids={ids}", i + 1);
batch.len()
})
.fold((0, 0), |(sent, batches), n| (sent + n, batches + 1));
println!("notified {sent} users in {batches} batches");
}batch 1: POST /notify ids=1,2,3,4
batch 2: POST /notify ids=5,6,7,8
batch 3: POST /notify ids=9,10,11
notified 11 users in 3 batches34. Realistic example: detecting anomalies with windows
Slices have windows(n), overlapping groups of n items. Comparing each reading with the average of the previous ones flags sudden spikes.
fn main() {
let requests_per_min = [120, 118, 125, 122, 480, 130, 127, 124, 20, 121];
let window = 3;
let anomalies: Vec<(usize, u32, f64)> = requests_per_min
.windows(window + 1)
.enumerate()
.filter_map(|(i, w)| {
let baseline = w[..window].iter().sum::<u32>() as f64 / window as f64;
let current = w[window];
let deviation = (current as f64 - baseline).abs() / baseline;
(deviation > 0.5).then_some((i + window, current, baseline))
})
.collect();
for (minute, value, baseline) in anomalies {
println!("minute {minute}: {value} req/min vs baseline {baseline:.0}");
}
}minute 4: 480 req/min vs baseline 122
minute 8: 20 req/min vs baseline 127condition.then_some(value) returns Some(value) if the condition is true and None otherwise, which fits filter_map neatly.
Summary
- Transform:
map,filter,filter_map,flat_map,flatten,enumerate,zip,chain,inspect. - Select:
take,skip,take_while,skip_while,step_by,rev,peekable,cycle. - Reduce:
fold,reduce,scan(running values),sum,product,count,min/maxand their_by_key/_byforms. - Search:
find,find_map,position,rposition,any,all,last,nth. - Split and collect:
partition,unzip,collectinto any collection, andjoinon slices of strings. - Slice helpers complete the toolbox:
chunks,windows,chunk_by,sort_by_key,dedup.
Next: Custom Iterators.
