Consuming Iterators

How to turn Rust iterators into concrete results using methods like collect, fold, sum, find, and partition

Iterators in Rust are lazy by design. Creating an iterator or chaining adapters like map and filter does no real work until something forces the iterator to yield its items and produce a final answer. The methods that trigger this evaluation are called consuming adaptors (or consumers). They take ownership of the iterator, run it to completion, and return a value—a number, a collection, a boolean flag, or an Option—rather than another iterator.

Understanding consuming methods gives you the exit point from a pipeline. Every chain that starts with iter() or into_iter() eventually needs a consumer at the end; otherwise the whole pipeline is inert.

How Consuming Methods Work

All consuming methods are defined on the Iterator trait and have one property in common: they accept self (not &self or &mut self), which means the iterator is moved into the method and cannot be used again afterwards. Internally they call next() repeatedly until None is returned, accumulating or deciding along the way.

This ownership transfer is not a limitation—it is the mechanism that guarantees you do not accidentally drive the same iterator twice. After a consumer returns, the iterator simply no longer exists.

Iterator adapters are not consumers:

Methods like map, filter, and take also take self but return a new iterator struct. They do not force evaluation; they merely wrap the original iterator inside a new one. Consuming methods return a non‑iterator value and therefore must evaluate everything.

Accumulating Values: count, sum, product

When you need a single number that summarises an entire sequence, these three methods are the simplest consumers.

count

Returns the number of items the iterator produced. It does not care about the item type at all; the only requirement is that the iterator implements Iterator (which all do).

let count = (0..100).filter(|x| x % 2 == 0).count();
println!("Even numbers under 100: {}", count); // 50

Behind the scenes count calls next() until exhaustion and increments a counter each time. There is no way to know the count without consuming the whole iterator because the length is not stored anywhere—this is why count consumes the iterator.

sum and product

sum adds all items together; product multiplies them. Both require the items to implement the std::iter::Sum or std::iter::Product trait, which the standard numeric types do automatically.

let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().copied().sum();
let mult: i32 = numbers.iter().copied().product();
assert_eq!(total, 15);
assert_eq!(mult, 120);

Notice the .copied() call. numbers.iter() yields &i32, but Sum<i32> is only implemented for iterators that yield owned i32 values. The copied() adapter turns Iterator<Item = &i32> into Iterator<Item = i32> by copying each reference. You can also use cloned() for non‑Copy types, or start with into_iter() if you are willing to give up the original collection.

sum on references:

If you write numbers.iter().sum::<i32>() you will get a compile error: Sum is not implemented for &i32. The quick fix is either into_iter() (consumes the vector) or .iter().copied().sum() (keeps the vector intact). Forgetting this is one of the most frequent early surprises when working with iterators.

Finding Extremes: max, min, and Custom Comparisons

When you need the largest or smallest item the iterator produced, max and min return an Option<Item> because an empty iterator has no meaningful extreme value. They require the item type to implement Ord.

let data = [7, 2, 9, 1];
assert_eq!(data.iter().max(), Some(&9));
assert_eq!(data.iter().min(), Some(&1));
assert_eq!(data.iter().copied().max(), Some(9)); // owned version

Custom key extraction with max_by_key and min_by_key

Often you want the extreme element according to a derived property, not the whole item. max_by_key and min_by_key accept a closure that maps each item to a comparable key. Internally they still walk the whole iterator once, tracking the best element.

let words = ["elephant", "cat", "dinosaur"];
let longest = words.iter().max_by_key(|w| w.len());
println!("Longest: {}", longest.unwrap()); // "elephant"

The closure is called once per item, and only the key values are compared. This is cleaner and often more efficient than calling max_by with a full comparison closure.

Full control with max_by and min_by

When you need a comparison that does not reduce to a single key, max_by and min_by give you a direct comparison function returning Ordering.

let pairs = [(3, 10), (1, 20), (4, 5)];
let max_by_second = pairs.iter().max_by(|a, b| a.1.cmp(&b.1));
assert_eq!(max_by_second, Some(&(1, 20)));

All these methods consume the iterator because they must examine every item to guarantee the result is correct. There is no shortcut for an unsorted sequence.

Testing All or Some Elements: any and all

any returns true if at least one item satisfies the predicate. all returns true only if every item does. Both methods short‑circuit: any stops as soon as it finds a true, and all stops at the first false.

let numbers = [2, 4, 6, 7, 8];
let all_even = numbers.iter().all(|&x| x % 2 == 0); // false, stops at 7
let any_odd = numbers.iter().any(|&x| x % 2 != 0); // true, stops at 7
println!("all_even: {}, any_odd: {}", all_even, any_odd);

Short‑circuiting is guaranteed:

Even though the iterator is consumed, the rest of the items after the decision point are never fetched. This makes any and all safe to use on iterators that produce a very large (or even infinite) number of items, as long as the answer is found early.

These are often the right tool when you would otherwise write a manual for loop with a break.

Locating Items: find, position, and nth

find

Returns the first item that matches a predicate, wrapped in Option. It consumes items until the predicate returns true, then stops.

let numbers = [1, 2, 3, 4];
let first_even = numbers.iter().find(|&&x| x % 2 == 0);
assert_eq!(first_even, Some(&2));

Because find moves the iterator forward, calling it a second time on the same iterator would continue from where it left off—if the iterator were still usable. Since it consumes the iterator, you cannot accidentally call it twice. If you need to search multiple times, you must create a new iterator or first collect the items into a data structure.

position

position works like find but returns the index of the matching item (zero‑based) instead of the item itself. It returns Option<usize>.

let data = ["apple", "banana", "cherry"];
let pos = data.iter().position(|&x| x == "banana");
assert_eq!(pos, Some(1));

rposition and ExactSizeIterator

rposition does the same as position but searches from the right‑hand side. It is only available on iterators that implement ExactSizeIterator, because it needs to know the length to work backwards. Ranges and slices provide this, but a general chain of adapters might lose the exact size.

let nums = [1, 2, 3, 2, 1];
let last_two = nums.iter().rposition(|&x| x == 2);
assert_eq!(last_two, Some(3)); // index of the last '2'

rposition requires ExactSizeIterator:

If you call rposition on an iterator that does not implement ExactSizeIterator, the compiler will reject the program. Chains that include filter, flat_map, or anything that can change the item count usually drop the ExactSizeIterator bound, so you cannot rely on rposition in those pipelines.

nth and last

nth(n) consumes the first n items and returns the next one (index n). It is useful when you want to skip ahead and grab a single element without collecting.

let range = 10..20;
let thirteenth = range.clone().nth(3); // 10,11,12, then 13
assert_eq!(thirteenth, Some(13));

last consumes the entire iterator and returns the final item. If the iterator implements DoubleEndedIterator, the standard library’s default implementation uses next_back for efficiency; otherwise it falls back to a naive walk to the end.

let data = [3, 1, 4, 1, 5];
assert_eq!(data.iter().copied().last(), Some(5));

The General Reduction: fold

fold is the most general consuming adaptor. Every other consumer could be implemented in terms of fold. It walks the iterator, maintaining an accumulator that is updated by a closure for each item. At the end, the accumulator becomes the return value.

let words = ["hello", " ", "world"];
let sentence: String = words.iter().fold(String::new(), |mut acc, w| {
    acc.push_str(w);
    acc
});
println!("{}", sentence); // "hello world"

The closure takes the current accumulator and the next item, and returns the new accumulator. The first argument to fold is the initial seed. Because the closure receives the accumulator by value and must return a new value, fold works well with owned data.

A more complex example: counting word frequencies using a HashMap:

use std::collections::HashMap;
let text = "apple banana apple cherry banana banana";
let freq = text.split_whitespace().fold(HashMap::new(), |mut map, word| {
    *map.entry(word).or_insert(0) += 1;
    map
});
println!("{:?}", freq); // {"apple": 2, "banana": 3, "cherry": 1}

Here the accumulator is the entire HashMap, and each step updates it. At the end we own the filled map.

fold moves the iterator into itself, so you cannot use the iterator afterwards. If you need to keep the iterator alive (for example, to call another consumer later), either clone the iterator before folding or—better—collect the items into a data structure first.

Collecting into a Collection: collect and FromIterator

collect is the workhorse for turning an iterator back into an owned collection. Its signature is generic: fn collect<B: FromIterator<Self::Item>>(self) -> B. The destination type B must implement the FromIterator trait, which defines how to build an instance from an iterator. All the standard collections (Vec, HashMap, HashSet, BTreeMap, String, etc.) implement FromIterator.

The most common usage is to produce a Vec:

let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]

The type annotation on the binding is often necessary because collect needs to know which collection you want. You can also use the turbofish syntax on the method:

let squares = (1..=5).map(|x| x * x).collect::<Vec<_>>();

Collecting into a HashMap

When the iterator yields (key, value) pairs, you can collect directly into a HashMap:

use std::collections::HashMap;
let pairs = vec![("one", 1), ("two", 2)];
let map: HashMap<_, _> = pairs.into_iter().collect();
println!("{:?}", map); // {"one": 1, "two": 2}

Collecting characters into a String

An iterator over char can be collected into a String:

let chars = ['H', 'e', 'l', 'l', 'o'];
let greeting: String = chars.iter().collect();
println!("{}", greeting); // "Hello"

collect without type annotation:

If you call collect() without enough type information, the compiler will produce an error like “type annotations needed”. This is because collect can turn into dozens of different collection types, and Rust cannot guess which one you mean. Either annotate the variable or use ::<CollectionType>().

The FromIterator trait is not limited to standard containers. Any type can implement it, which allows your own types to be built from an iterator. For example, a Result<Vec<T>, E> can be collected from an iterator of Result<T, E>, stopping at the first error:

let items = [Ok(1), Ok(2), Err("fail"), Ok(3)];
let result: Result<Vec<_>, &str> = items.into_iter().collect();
assert_eq!(result, Err("fail"));

This works because Result implements FromIterator in a way that accumulates Ok values into the inner collection, but short‑circuits on the first Err.

Filling an Existing Collection: the Extend Trait

While collect builds a new container from scratch, extend takes an existing collection and adds all iterator items into it. extend is not a method on Iterator itself; it is a trait (Extend) that collections implement, and you call it on the collection, passing the iterator.

let mut numbers = vec![1, 2, 3];
let more = [4, 5];
numbers.extend(more);
assert_eq!(numbers, vec![1, 2, 3, 4, 5]);

extend consumes the iterator, so after the call the iterator is gone, but the original collection is still usable and now contains the extra items. This is particularly useful when you are accumulating results across multiple separate iterator pipelines into the same container.

extend vs collect:

When you need to merge multiple iterator results into a single container without creating intermediate collections, extend is often more efficient and clearer than repeated collect calls followed by manual insertion.

Splitting into Two: partition

partition separates an iterator into two collections based on a predicate. Items for which the predicate returns true go into the first collection; false items go into the second. It returns a tuple (B, B) where both halves have the same type, which must implement Default + Extend<Item>.

let numbers = [1, 2, 3, 4, 5, 6];
let (evens, odds): (Vec<_>, Vec<_>) = numbers.iter().partition(|&x| x % 2 == 0);
println!("evens: {:?}, odds: {:?}", evens, odds);
// evens: [2, 4, 6], odds: [1, 3, 5]

Type annotation is often necessary for the same reason as collect: partition needs to know what kind of container to create for the two halves.

If you only need one side, filter plus collect is usually clearer than partition followed by discarding one half, but partition shines when both subsets are required simultaneously.

Comparing Entire Iterator Sequences

Iterator provides a family of consuming comparison methods that treat two sequences as if they were being compared element‑by‑element: eq, ne, lt, le, gt, ge, and the more general cmp, partial_cmp, and their _by variants. They consume the iterator and compare it with another IntoIterator.

let a = [1, 2, 3];
let b = [1, 2, 4];
assert!(a.iter().lt(&b));   // true: 3 < 4
assert!(a.iter().ne(&b));   // true

These methods are useful when you want to implement ordering for your own types by delegating to an iterator over their components, or when comparing large sequences without materialising intermediate collections.

Lexicographic comparison:

The comparison stops at the first differing element. If one sequence is a prefix of the other, the shorter sequence is considered “less”. This is the same behaviour as slice comparison but expressed via iterators.


Rust’s consuming adaptors close the loop on the iterator story. They are not an afterthought; they are the reason the lazy pipeline exists. Every chain of map and filter eventually meets a consumer that forces evaluation, produces a result, and releases the resources.

If you take away one insight, let it be this: the choice of consumer determines what you get back, but the way you build the iterator determines what you pay. By keeping the iterator chain lazy, you avoid intermediate allocations, and the right consumer—fold instead of collect followed by a loop, or any instead of filter followed by count > 0—can eliminate work entirely through short‑circuiting.