Iterator Consumers: From Array Methods to collect, fold, and Friends
21 min read
The sibling page on iterators covered the lazy adaptors — map, filter, take, zip — that build a pipeline but compute nothing on their own. This page covers the other half: the consuming adaptors (or consumers) that actually run the pipeline and hand you a result. These are the methods that line up with reduce, find, some, every, and the implicit “now give me an array” that every JavaScript chain ends with.
Quick Overview
Section titled “Quick Overview”A consumer is an iterator method that takes ownership of the iterator (self, not &mut self), drives it to completion (or until it can short-circuit), and produces a final value — a number, a bool, an Option, or a whole new collection. In TypeScript an array method like .reduce() or .filter() always produces a value immediately; in Rust the lazy chain does nothing until a consumer like collect, sum, fold, find, any, all, or max is called at the end. The consumer is what makes the whole pipeline run — without one, the compiler will warn you that your iterator was never used.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”Here is a realistic chunk of e-commerce analytics: given a list of orders, compute several aggregates. Notice every line produces a value eagerly, and that .reduce() on an empty array throws.
interface Order { id: number; customer: string; totalCents: number; paid: boolean;}
const orders: Order[] = [ { id: 1, customer: "alice", totalCents: 4999, paid: true }, { id: 2, customer: "bob", totalCents: 12000, paid: false }, { id: 3, customer: "alice", totalCents: 8050, paid: true }, { id: 4, customer: "carol", totalCents: 0, paid: true },];
// count → .filter(...).lengthconst paidCount = orders.filter((o) => o.paid).length;
// sum → .reduce(...)const revenue = orders .filter((o) => o.paid) .reduce((sum, o) => sum + o.totalCents, 0);
// any / all → .some() / .every()const hasUnpaid = orders.some((o) => !o.paid);const allHaveCustomer = orders.every((o) => o.customer.length > 0);
// find → .find() (returns the element or undefined)const firstBig = orders.find((o) => o.totalCents > 5000);
// max → .reduce() with a comparator (no built-in maxBy!)const priciest = orders.reduce((best, o) => o.totalCents > best.totalCents ? o : best);
// build a new array → .filter(...).map(...)const paidCustomers = orders.filter((o) => o.paid).map((o) => o.customer);Two things to keep in mind, because Rust handles them differently: .find() returns undefined when nothing matches (no error), and .reduce() with no initial value on an empty array throws a TypeError.
Rust Equivalent
Section titled “Rust Equivalent”#[derive(Debug)]struct Order { id: u32, customer: String, total_cents: u64, paid: bool,}
fn main() { let orders = vec![ Order { id: 1, customer: "alice".into(), total_cents: 4_999, paid: true }, Order { id: 2, customer: "bob".into(), total_cents: 12_000, paid: false }, Order { id: 3, customer: "alice".into(), total_cents: 8_050, paid: true }, Order { id: 4, customer: "carol".into(), total_cents: 0, paid: true }, ];
// count: how many orders are paid? let paid_count = orders.iter().filter(|o| o.paid).count();
// sum: total revenue from paid orders, in cents. let revenue: u64 = orders .iter() .filter(|o| o.paid) .map(|o| o.total_cents) .sum();
// any / all: quick boolean checks that short-circuit. let has_unpaid = orders.iter().any(|o| !o.paid); let all_have_customer = orders.iter().all(|o| !o.customer.is_empty());
// find: first matching element, as Option<&Order>. let first_big = orders.iter().find(|o| o.total_cents > 5_000);
// max_by_key: the priciest order (Option, because the list could be empty). let priciest = orders.iter().max_by_key(|o| o.total_cents);
// collect: build a brand-new Vec of just the paid customers' names. let paid_customers: Vec<&str> = orders.iter().filter(|o| o.paid).map(|o| o.customer.as_str()).collect();
// fold: a running custom aggregation (here, a formatted receipt). let receipt = orders.iter().fold(String::new(), |mut acc, o| { acc.push_str(&format!("#{} ${:.2}\n", o.id, o.total_cents as f64 / 100.0)); acc });
println!("paid_count = {paid_count}"); println!("revenue (cents) = {revenue}"); println!("has_unpaid = {has_unpaid}"); println!("all_have_cust = {all_have_customer}"); println!("first_big = {:?}", first_big.map(|o| o.id)); println!("priciest = {:?}", priciest.map(|o| o.id)); println!("paid_customers = {paid_customers:?}"); print!("{receipt}");}Real output:
paid_count = 3revenue (cents) = 13049has_unpaid = trueall_have_cust = truefirst_big = Some(2)priciest = Some(2)paid_customers = ["alice", "alice", "carol"]#1 $49.99#2 $120.00#3 $80.50#4 $0.00The aggregate numbers match the TypeScript version exactly (paid_count = 3, revenue = 13049). The differences are all about types: find and max_by_key give you an Option, never undefined; count returns a usize; and revenue needs an explicit u64 annotation so sum knows what to add into.
Detailed Explanation
Section titled “Detailed Explanation”collect — the universal “materialize” consumer
Section titled “collect — the universal “materialize” consumer”In TypeScript a chain ends in an array because the methods return arrays. In Rust the chain ends in .collect(), which can build many different collections — Vec, String, HashMap, HashSet, even Result — depending on the target type you ask for:
use std::collections::{HashMap, HashSet};
fn main() { let v: Vec<i32> = (1..=5).collect(); let s: String = vec!['h', 'i', '!'].into_iter().collect(); let set: HashSet<i32> = vec![1, 2, 2, 3, 3, 3].into_iter().collect(); let map: HashMap<&str, i32> = vec![("a", 1), ("b", 2)].into_iter().collect();
println!("v={v:?}"); println!("s={s}"); println!("set.len()={}", set.len()); println!("map.get(b)={:?}", map.get("b"));
// Turbofish form: annotate the call instead of the binding. let doubled = (1..=3).map(|n| n * 2).collect::<Vec<_>>(); println!("doubled={doubled:?}");}v=[1, 2, 3, 4, 5]s=hi!set.len()=3map.get(b)=Some(2)doubled=[2, 4, 6]collect is generic over the trait FromIterator. Because the same call can produce a Vec, a String, a HashMap, and so on, you must tell Rust which one you want — either with a binding annotation (let v: Vec<i32> =) or with the ::<Vec<_>> “turbofish”. This is the single biggest surprise for newcomers, and it has no TypeScript analogue, where .map() always returns an array.
Tip: When the element type is obvious but the container is not, use
collect::<Vec<_>>(). The_lets the compiler infer the element type while you pin the container.
collect into Result — short-circuiting validation
Section titled “collect into Result — short-circuiting validation”One of the most useful tricks: an iterator of Results can collect into a single Result<Vec<_>, E>. The first Err stops the process and becomes the whole result. This is the idiomatic way to “parse every item, but bail on the first failure”:
fn main() { let good: Result<Vec<i32>, _> = vec!["1", "2", "3"].iter().map(|s| s.parse::<i32>()).collect(); let bad: Result<Vec<i32>, _> = vec!["1", "x", "3"].iter().map(|s| s.parse::<i32>()).collect();
println!("good={good:?}"); println!("bad is_err={}", bad.is_err());}good=Ok([1, 2, 3])bad is_err=trueThere is no clean JavaScript equivalent — you would reach for a for loop with a try/catch, or Promise.all semantics if it were async. See Section 08 — Error Handling for why this pattern is everywhere in Rust.
sum, product, count — numeric reductions
Section titled “sum, product, count — numeric reductions”fn main() { let total: i32 = (1..=5).sum(); // 1+2+3+4+5 let fact: u64 = (1..=5u64).product(); // 5! let evens = (1..=10).filter(|n| n % 2 == 0).count();
println!("total={total} fact={fact} evens={evens}");}total=15 fact=120 evens=5sum and product are like collect: they are generic over the output type, so you usually annotate it (let total: i32). count always returns a usize, so it never needs annotating. Each replaces a reduce you would write by hand in JavaScript.
min, max, and the _by / _by_key family
Section titled “min, max, and the _by / _by_key family”fn main() { let nums = vec![3, 7, 2, 9, 4]; println!("min={:?} max={:?}", nums.iter().min(), nums.iter().max());
let words = vec!["pear", "fig", "banana"]; println!("longest={:?}", words.iter().max_by_key(|w| w.len())); println!("shortest={:?}", words.iter().min_by_key(|w| w.len()));
// f64 isn't `Ord`, so plain `.max()` won't compile — fold with f64::max. let temps = vec![19.5_f64, 22.0, 18.0]; let hottest = temps.iter().cloned().fold(f64::MIN, f64::max); println!("hottest={hottest}");}min=Some(2) max=Some(9)longest=Some("banana")shortest=Some("fig")hottest=22JavaScript has no Array.prototype.maxBy; you write .reduce() with a comparator. Rust gives you the whole family directly:
min/max— compare elements with their natural ordering (Ord).min_by_key/max_by_key— compare a derived key (cheap, computed once per element).min_by/max_by— compare with a custom|a, b| a.cmp(b)closure returningOrdering.
All of them return Option<T> (None for an empty iterator). The f64 case is a real gotcha covered under Common Pitfalls: floats are not totally ordered (because of NaN), so f64 does not implement Ord, and .max() won’t compile.
find, position, find_map — “give me the first one that…”
Section titled “find, position, find_map — “give me the first one that…””fn main() { let data = vec!["", " ", "hello", "world"];
// find: first ELEMENT matching the predicate. let first_nonblank = data.iter().find(|s| !s.trim().is_empty());
// position: INDEX of the first match (like Array.prototype.findIndex). let idx = data.iter().position(|s| !s.trim().is_empty());
// find_map: first item where the closure returns Some(...). let parsed = vec!["x", "12", "y"].iter().find_map(|s| s.parse::<i32>().ok());
println!("first_nonblank={first_nonblank:?}"); println!("idx={idx:?}"); println!("parsed={parsed:?}");}first_nonblank=Some("hello")idx=Some(2)parsed=Some(12)find ≈ Array.prototype.find, position ≈ findIndex, and find_map has no single JS equivalent — it is find and map fused so you compute the transformed value exactly once for the first match. All of them short-circuit: they stop the moment they have an answer, never touching the rest of the iterator.
any / all — the boolean short-circuiters
Section titled “any / all — the boolean short-circuiters”fn main() { let nums = vec![2, 4, 6, 8]; let has_odd = nums.iter().any(|n| n % 2 == 1); // like .some() let all_even = nums.iter().all(|n| n % 2 == 0); // like .every()
// Edge cases on an EMPTY iterator — note the defaults! let any_empty = std::iter::empty::<i32>().any(|n| n > 0); let all_empty = std::iter::empty::<i32>().all(|n| n > 0);
println!("has_odd={has_odd} all_even={all_even}"); println!("any_empty={any_empty} all_empty={all_empty}");}has_odd=false all_even=trueany_empty=false all_empty=trueThese behave exactly like JavaScript’s some/every, including the “vacuous truth” defaults: any on an empty iterator is false, all on an empty iterator is true.
fold vs reduce — the key distinction
Section titled “fold vs reduce — the key distinction”This is where Rust splits one JavaScript method into two:
fn main() { // fold: you SUPPLY a seed, so the result type can differ from the items. let folded = (1..=4).fold(100, |acc, n| acc + n); // 100 + 1+2+3+4
// reduce: NO seed; the first item is the seed. Returns Option. let reduced = (1..=4).reduce(|acc, n| acc + n);
// reduce on an EMPTY iterator returns None — it does NOT panic. let empty_reduced = std::iter::empty::<i32>().reduce(|a, b| a + b);
println!("folded={folded} reduced={reduced:?} empty_reduced={empty_reduced:?}");}folded=110 reduced=Some(10) empty_reduced=Nonefold(init, f)≈ JavaScriptreduce(f, initialValue). The accumulator type comes frominit, so it can be anything — a number, aString, aHashMap. This is the workhorse for building up custom aggregates (the receipt-building example earlier folds into aString).reduce(f)≈ JavaScriptreduce(f)without an initial value. Because there might be zero elements, Rust returnsOption<T>instead of throwing. This is the crucial safety difference: JavaScript’s seedless[].reduce(...)throws aTypeError; Rust’sreducequietly gives youNone.
Reach for fold by default. Reach for reduce only when there is no sensible identity value (e.g. “combine these UI nodes into one”) and you genuinely want the Option.
Key Differences
Section titled “Key Differences”| Concept | TypeScript / JavaScript | Rust | Notes |
|---|---|---|---|
| Build a collection | implicit — methods return arrays | explicit .collect() | must annotate the target type |
| Multiple target types | always Array | Vec, String, HashMap, HashSet, Result… | one method, many FromIterator impls |
| Sum | arr.reduce((a, b) => a + b, 0) | .sum::<T>() | annotate the numeric type |
| Count | arr.length after filtering | .count() | returns usize |
| First match (value) | .find() → element | undefined | .find() → Option<T> | no undefined/null |
| First match (index) | .findIndex() → number (-1 miss) | .position() → Option<usize> | None, not -1 |
| Some / every | .some() / .every() | .any() / .all() | identical short-circuit semantics |
| Max element | .reduce() with comparator | .max(), .max_by_key(), .max_by() | built-in; returns Option |
| Reduce with seed | .reduce(f, init) | .fold(init, f) | accumulator type = seed type |
| Reduce without seed | .reduce(f) — throws if empty | .reduce(f) → Option<T> | None instead of a TypeError |
| Laziness | eager — runs at each call | lazy — runs only at the consumer | the consumer drives the chain |
Note: The deepest difference is laziness. A JavaScript
.filter().map()builds two intermediate arrays immediately. A Rust.filter().map()builds nothing — it is a description of work that only happens when a consumer likecollect,sum, orforpulls items through. See Iterators for the full story on lazy adaptors.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: collect (or sum) with no type annotation
Section titled “Pitfall 1: collect (or sum) with no type annotation”The compiler cannot guess which collection or numeric type you want:
fn main() { let doubled = (1..=3).map(|n| n * 2).collect(); // does not compile (error[E0283]) println!("{doubled:?}");}Real rustc output:
error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let doubled = (1..=3).map(|n| n * 2).collect(); | ^^^^^^^ ------- type must be known at this point | = note: cannot satisfy `_: FromIterator<i32>`help: consider giving `doubled` an explicit type |2 | let doubled: Vec<_> = (1..=3).map(|n| n * 2).collect(); | ++++++++The same E0283 appears for sum:
fn main() { let nums = vec![1, 2, 3]; let total = nums.iter().sum(); // does not compile (error[E0283]: type annotations needed) println!("{total}");}error[E0283]: type annotations needed --> src/main.rs:3:9 |3 | let total = nums.iter().sum(); | ^^^^^ --- type must be known at this point | = note: cannot satisfy `_: Sum<&i32>`help: consider giving `total` an explicit type |3 | let total: /* Type */ = nums.iter().sum(); | ++++++++++++Fix: annotate the binding (let total: i32) or use a turbofish (.sum::<i32>(), .collect::<Vec<_>>()).
Pitfall 2: using an iterator after a consumer has eaten it
Section titled “Pitfall 2: using an iterator after a consumer has eaten it”Consumers take self by value. Once you call one, the iterator is moved and gone — you cannot call a second consumer on the same iterator:
fn main() { let nums = vec![1, 2, 3]; let it = nums.iter(); let count = it.count(); // count() consumes `it` let total: i32 = it.sum(); // does not compile (error[E0382]: use of moved value) println!("{count} {total}");}Real rustc output (trimmed):
error[E0382]: use of moved value: `it` --> src/main.rs:5:22 |3 | let it = nums.iter(); | -- move occurs because `it` has type `std::slice::Iter<'_, i32>`, which does not implement the `Copy` trait4 | let count = it.count(); // count() consumes `it` | ------- `it` moved due to this method call5 | let total: i32 = it.sum(); // | ^^ value used here after moveFix: make a fresh iterator each time (nums.iter().count() then nums.iter().sum()), since iter() only borrows the Vec. In TypeScript you would just call two methods on the same array; an iterator is single-use, more like a generator you have already exhausted.
Pitfall 3: min/max on floats
Section titled “Pitfall 3: min/max on floats”f64 and f32 are not Ord (because NaN breaks total ordering), so .min() / .max() simply do not exist for them:
fn main() { let temps = vec![19.5_f64, 22.0, 18.0]; let hottest = temps.iter().max(); // does not compile (error[E0277]: f64: Ord not satisfied) println!("{hottest:?}");}error[E0277]: the trait bound `f64: Ord` is not satisfied --> src/main.rs:3:32 |3 | let hottest = temps.iter().max(); | ^^^ the trait `Ord` is not implemented for `f64`Fix: decide how to treat NaN yourself. The simplest is to fold with the partial-order-aware f64::max: temps.iter().cloned().fold(f64::MIN, f64::max). Or use .max_by(|a, b| a.partial_cmp(b).unwrap()) if you are certain there are no NaNs. This is a place where the analogy to JavaScript’s Math.max(...arr) (which silently returns NaN if any element is NaN) breaks down — Rust forces you to confront the ambiguity.
Pitfall 4: forgetting the consumer entirely
Section titled “Pitfall 4: forgetting the consumer entirely”A chain of lazy adaptors with no consumer does nothing, and the compiler warns:
fn main() { let nums = vec![1, 2, 3]; nums.iter().map(|n| println!("{n}")); // runs NOTHING; warning: unused `Map`}Rust emits warning: unused 'Map' that must be used with the note iterators are lazy and do nothing unless consumed. If you wanted side effects, use a for loop or the for_each consumer: nums.iter().for_each(|n| println!("{n}"));. Coming from JavaScript — where .map() always executes — this is the most common “why is nothing happening?” moment.
Best Practices
Section titled “Best Practices”- Pick the most specific consumer. Prefer
count()over.collect::<Vec<_>>().len(),sum()over a hand-rolledfold, andmax_by_keyoverfoldwith a manual comparison. They are clearer and let the compiler optimize. - Annotate the output type at the consumer.
let total: u64 = ...or.sum::<u64>(). Decide the integer width deliberately to avoid overflow on large sums. - Use
collect::<Result<Vec<_>, _>>()for fallible pipelines. It short-circuits on the first error and keeps the happy path flat — no manual loop with early returns. - Reach for
foldoverreduce.foldis total (always returns a value) and lets the accumulator be any type. Usereduceonly when there is genuinely no identity element and you want theOption. partitionsplits in one pass. When you would write twofilters, usepartitioninstead — it walks the iterator once and returns a(matches, rest)tuple:
fn main() { let nums = vec![1, 2, 3, 4, 5, 6]; let (evens, odds): (Vec<i32>, Vec<i32>) = nums.iter().partition(|&&n| n % 2 == 0); println!("evens={evens:?} odds={odds:?}");}evens=[2, 4, 6] odds=[1, 3, 5]- Use
try_foldfor short-circuiting accumulation. It stops at the firstNone/Err, which is perfect for checked arithmetic or validation that builds state:
fn main() { let nums = vec![1, 2, 3, 4]; let checked: Option<i32> = nums.iter().try_fold(0i32, |acc, &n| acc.checked_add(n)); println!("{checked:?}"); // Some(10); None if any add overflowed}Some(10)Tip: Run
cargo clippy. It will nudge you toward the idiomatic consumer — e.g. flagging afoldthat should be asum(via the defaultclippy::unnecessary_foldlint). And with the nursery lintclippy::needless_collectenabled (-W clippy::needless_collect), it suggests.count()instead of collecting just to call.len().
Real-World Example
Section titled “Real-World Example”A small log-analysis pass: parse raw lines into structured entries, drop the malformed ones, and compute a handful of metrics — every one of them a different consumer.
use std::collections::HashMap;
#[derive(Debug, Clone)]struct LogEntry { status: u16, bytes: u64, path: String,}
fn parse(line: &str) -> Option<LogEntry> { let mut parts = line.split_whitespace(); let status = parts.next()?.parse::<u16>().ok()?; let bytes = parts.next()?.parse::<u64>().ok()?; let path = parts.next()?.to_string(); Some(LogEntry { status, bytes, path })}
fn main() { let raw = "\200 1024 /home404 0 /missing200 2048 /home500 0 /api200 512 /aboutbad line301 128 /old";
// Parse every line, silently dropping the ones that don't parse (filter_map). let entries: Vec<LogEntry> = raw.lines().filter_map(parse).collect();
// sum: total bytes served. let total_bytes: u64 = entries.iter().map(|e| e.bytes).sum();
// any: did anything 5xx happen? (short-circuits) let has_5xx = entries.iter().any(|e| e.status >= 500);
// count + count: success rate. let ok = entries.iter().filter(|e| (200..300).contains(&e.status)).count(); let success_rate = ok as f64 / entries.len() as f64;
// fold into a HashMap, then max_by_key: the busiest path. let hits = entries.iter().fold(HashMap::<&str, u32>::new(), |mut acc, e| { *acc.entry(e.path.as_str()).or_insert(0) += 1; acc }); let busiest = hits.iter().max_by_key(|(_, count)| **count);
// partition: separate redirects from everything else, in one pass. let (redirects, others): (Vec<_>, Vec<_>) = entries.iter().partition(|e| (300..400).contains(&e.status));
println!("parsed entries = {}", entries.len()); println!("total_bytes = {total_bytes}"); println!("has_5xx = {has_5xx}"); println!("success_rate = {success_rate:.2}"); println!("busiest = {:?}", busiest.map(|(p, c)| (*p, *c))); println!("redirects = {}", redirects.len()); println!("others = {}", others.len());}Real output:
parsed entries = 6total_bytes = 3712has_5xx = truesuccess_rate = 0.50busiest = Some(("/home", 2))redirects = 1others = 5Every metric is a consumer doing one job: filter_map(...).collect() to materialize, sum for bytes, any for the 5xx check, count for the rate, fold + max_by_key for the busiest path, and partition for the split. In TypeScript this would be a mix of .filter().map(), manual .reduce() accumulators, and an object-as-map for the counts — and parse returning Option would instead be null-checks scattered through the pipeline.
Note: Building the frequency map with
foldhere is idiomatic, but for grouping you will often reach for theentryAPI directly in a loop — see HashMaps. Andfilter_map(parse)works becauseparsereturnsOption; that is the consumer-side payoff of returningOptionfrom fallible helpers, covered in Section 08 — Error Handling.
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”Iteratortrait — all consuming methods —collect,fold,sum,find,any,all,max, and the restFromIteratortrait — what makescollectpolymorphic over the target type- The Rust Book — Processing a Series of Items with Iterators
- Rust by Example — Iterators
Related Topics in This Guide
Section titled “Related Topics in This Guide”- Iterators — the lazy adaptors (
map,filter,take,zip,enumerate) that feed these consumers - Custom Iterators — implement
Iteratorfor your own types so every consumer here works on them - Vectors —
Vec<T>, the most common thing youcollectinto - HashMaps and HashSets — also valid
collecttargets - Collection Performance — when an iterator chain beats a manual loop, and pre-sizing
collect - Section 08 — Error Handling —
collect::<Result<_, _>>()and returningOption/Resultfrom pipeline helpers - Section 02 — Basics: Types —
usize,Option, and whyf64is notOrd - Section 01 — Getting Started — setting up
cargoto run these examples
Exercises
Section titled “Exercises”Exercise 1: Order statistics
Section titled “Exercise 1: Order statistics”Difficulty: Beginner
Objective: Combine sum, max, and count (via len) into one function, handling the empty case.
Instructions: Write order_stats(prices: &[u32]) -> (u32, u32, f64) returning (total, max, average). For an empty slice it must return (0, 0, 0.0) and must not panic. (Hint: max() returns an Option; use unwrap_or(0).)
fn order_stats(prices: &[u32]) -> (u32, u32, f64) { // TODO: total via sum, max via max(), average guarding against empty todo!()}
fn main() { let (t, m, a) = order_stats(&[1200, 950, 4000, 300]); println!("total={t} max={m} avg={a:.2}"); assert_eq!(order_stats(&[]), (0, 0, 0.0)); println!("ok");}Solution
fn order_stats(prices: &[u32]) -> (u32, u32, f64) { let count = prices.len(); let total: u32 = prices.iter().sum(); let max = prices.iter().copied().max().unwrap_or(0); let avg = if count == 0 { 0.0 } else { total as f64 / count as f64 }; (total, max, avg)}
fn main() { let (t, m, a) = order_stats(&[1200, 950, 4000, 300]); println!("total={t} max={m} avg={a:.2}"); assert_eq!(order_stats(&[]), (0, 0, 0.0)); println!("ok");}Output:
total=6450 max=4000 avg=1612.50okExercise 2: Validate and sum in one pass
Section titled “Exercise 2: Validate and sum in one pass”Difficulty: Intermediate
Objective: Use collect into a Result to parse a list of strings, bailing on the first bad one.
Instructions: Write parse_all(tokens: &[&str]) -> Result<i32, std::num::ParseIntError> that parses every token as i32 and returns their sum, or the first parse error. Do not write a manual loop with early returns — let collect do the short-circuiting.
fn parse_all(tokens: &[&str]) -> Result<i32, std::num::ParseIntError> { // TODO: map -> collect into Result<Vec<i32>, _>, then sum todo!()}
fn main() { assert_eq!(parse_all(&["1", "2", "3"]).unwrap(), 6); assert!(parse_all(&["1", "oops", "3"]).is_err()); println!("ok");}Solution
fn parse_all(tokens: &[&str]) -> Result<i32, std::num::ParseIntError> { let nums: Vec<i32> = tokens .iter() .map(|t| t.parse::<i32>()) .collect::<Result<_, _>>()?; Ok(nums.iter().sum())}
fn main() { assert_eq!(parse_all(&["1", "2", "3"]).unwrap(), 6); assert!(parse_all(&["1", "oops", "3"]).is_err()); println!("ok");}The ? unwraps the Result<Vec<i32>, _> produced by collect; if any token failed to parse, that error is returned immediately. Output:
okExercise 3: Most common word
Section titled “Exercise 3: Most common word”Difficulty: Advanced
Objective: Chain a transforming pipeline with a fold-built frequency map and a max_by_key consumer.
Instructions: Write most_common_word(text: &str) -> Option<(String, u32)> that lowercases words, strips surrounding punctuation, ignores empty tokens, counts occurrences, and returns the most frequent (word, count) — or None for empty input. (Hints: split_whitespace, trim_matches, to_lowercase, fold into a HashMap<String, u32>, then into_iter().max_by_key(...).)
use std::collections::HashMap;
fn most_common_word(text: &str) -> Option<(String, u32)> { // TODO: normalize words, fold into a count map, then max_by_key todo!()}
fn main() { let text = "The fox, the hound, and THE FOX!"; println!("{:?}", most_common_word(text)); assert_eq!(most_common_word(""), None); println!("ok");}Solution
use std::collections::HashMap;
fn most_common_word(text: &str) -> Option<(String, u32)> { let counts = text .split_whitespace() .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase()) .filter(|w| !w.is_empty()) .fold(HashMap::<String, u32>::new(), |mut acc, w| { *acc.entry(w).or_insert(0) += 1; acc });
counts.into_iter().max_by_key(|(_, count)| *count)}
fn main() { let text = "The fox, the hound, and THE FOX!"; println!("{:?}", most_common_word(text)); assert_eq!(most_common_word(""), None); println!("ok");}Output:
Some(("the", 3))okNote:
max_by_keyreturns some maximal element when several tie; which one is unspecified forHashMapiteration order. If you need deterministic tie-breaking, fold into aBTreeMapor sort first.