Skip to content
Learn Rust
Theme

Iterators

Working with collections · Lesson 31

An iterator hands out items one at a time, and does nothing until something asks for them. You chain steps like filter, map, zip and fold onto it instead of writing the loop. The only surprise is how many & the closure receives.

Coming from GoGo's standard library has no map or filter for slices, so this replaces a hand-written loop. The chain is lazy: nothing runs until .collect() or .sum(), and the closure often gets a reference, like &i32, instead of the number.

Coming from KotlinThis is Kotlin's filter and map, but lazy like a Sequence: you start with .iter() and finish with .collect(). There is no it, and the closure gets references, so filter sees &&i32.

Coming from PythonA chain like .iter().filter(...).map(...) is lazy like a generator expression, and .collect() is list(...). The surprise is that closures get references, so you write |&&n| or **n to reach the number.

Coming from TypeScriptArray filter and map build a new array at each step, but a Rust chain is lazy until .collect() or .sum(). You start with .iter(), and the closure gets references (&&i32 in filter) instead of plain numbers.

Coming from Javaiter().filter().map().collect() is a Stream pipeline ending in toList(), and it's just as lazy. The catch is that items are references, so filter gets &&i32 and you peel them off with patterns like |&&n|.

Coming from C#filter is Where and map is Select, lazy like LINQ, and collect() is the ToList() at the end that runs the whole chain. The catch is that .iter() yields references, so filter gets &&i32 and you write |&&n| or **n to get the number.

Coming from CC has no closures or iterator chains, so you'd write one loop that tests and stores. Rust's chain compiles down to much the same loop; the cost is getting used to closures that receive references like &&i32.

Coming from C++filter and map are std::views::filter and std::views::transform, lazy in the same way, and collect() is C++23's std::ranges::to. A C++ lambda taking auto n gets a copy of each int; Rust's filter lends you a reference to the item instead, so you write |&&n| to get the number.

The quick comparison

Rustsrc/main.rsRun ↗
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
let evens_squared: Vec<i32> = nums
.iter() // items are &i32 (borrowed from nums)
.filter(|&&n| n % 2 == 0) // filter lends each item again: &&i32
.map(|n| n * n) // map gets the item itself: &i32
.collect(); // build a Vec from the results
let total: i32 = nums.iter().sum();
println!("{evens_squared:?} {total}");
}
$ cargo run
[4, 16, 36] 21
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
var evensSquared []int
total := 0
for _, n := range nums {
if n%2 == 0 {
evensSquared = append(evensSquared, n*n)
}
total += n
}
fmt.Println(evensSquared, total)
}
$ go run main.go
[4 16 36] 21
fun main() {
val nums = listOf(1, 2, 3, 4, 5, 6)
val evensSquared = nums
.filter { it % 2 == 0 }
.map { it * it }
val total = nums.sum()
println("$evensSquared $total")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
[4, 16, 36] 21
nums = [1, 2, 3, 4, 5, 6]
evens_squared = [n * n for n in nums if n % 2 == 0]
total = sum(nums)
print(f"{evens_squared} {total}")
$ python3 main.py
[4, 16, 36] 21
const nums = [1, 2, 3, 4, 5, 6];
// each step builds a new array right away (not lazy like Rust)
const evensSquared = nums
.filter((n) => n % 2 === 0)
.map((n) => n * n);
const total = nums.reduce((sum, n) => sum + n, 0);
console.log(evensSquared, total);
$ tsc --strict --module nodenext --types node main.ts && node main.js
[ 4, 16, 36 ] 21
import java.util.List;
public class Main {
public static void main(String[] args) {
var nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evensSquared = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.toList();
int total = nums.stream().mapToInt(Integer::intValue).sum();
System.out.println(evensSquared + " " + total);
}
}
$ javac Main.java && java Main
[4, 16, 36] 21
List<int> nums = [1, 2, 3, 4, 5, 6];
var evensSquared = nums
.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList(); // build a List from the results
var total = nums.Sum();
Console.WriteLine($"[{string.Join(", ", evensSquared)}] {total}");
$ dotnet run
[4, 16, 36] 21
#include <stdio.h>
int main(void) {
int nums[] = {1, 2, 3, 4, 5, 6};
// C has no map/filter: one loop does the filter and the map
int evens_squared[6];
int count = 0;
for (int i = 0; i < 6; i++) {
if (nums[i] % 2 == 0) {
evens_squared[count++] = nums[i] * nums[i];
}
}
int total = 0;
for (int i = 0; i < 6; i++) {
total += nums[i];
}
printf("[");
for (int i = 0; i < count; i++) {
printf(i ? ", %d" : "%d", evens_squared[i]);
}
printf("] %d\n", total);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
[4, 16, 36] 21
#include <iostream>
#include <numeric>
#include <ranges>
#include <vector>
int main() {
std::vector<int> nums{1, 2, 3, 4, 5, 6};
auto view = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; }); // lazy, like a Rust iterator
std::vector<int> evens_squared(view.begin(), view.end()); // collect
int total = std::accumulate(nums.begin(), nums.end(), 0);
std::cout << "[";
for (std::size_t i = 0; i < evens_squared.size(); ++i) {
std::cout << (i ? ", " : "") << evens_squared[i];
}
std::cout << "] " << total << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
[4, 16, 36] 21

What's different

  • Why &&? .iter() lends you each item, so it is &i32. filter has to look at the item and then still pass it on, so it can't hand it over. It lends it to your closure again. A lend of a lend is &&i32.
  • map takes the item and returns something new, so it gets the item as-is (&i32 here). Rule: filter, find, take_while get one extra &. map, for_each, fold don't.
  • |&&n| is a pattern that strips both & and gives you the plain number. The toolkit below explains references in iterators properly, and how to avoid them with .copied() or .into_iter().
  • Chains are lazy. Nothing runs until collect() or sum(). In a release build they typically compile to the same code as a hand-written loop.
Gotcha

Doing arithmetic on what filter hands you

Looks fine: in Kotlin, Python, TypeScript, Java, C# and C++ it runs fine. Rust won't compile it.

No direct equivalent in Go. Just a Rust rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.No direct equivalent in C. Just a Rust rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = nums.iter().filter(|n| n % 2 == 0).collect();
println!("{evens:?}");
}
Gono equivalent

Go has no filter in its standard library. You would write a loop, where n is just the number.

fun main() {
val nums = listOf(1, 2, 3, 4, 5, 6)
val evens = nums.filter { n -> n % 2 == 0 }
println(evens)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
[2, 4, 6]
nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, nums))
print(evens)
$ python3 main.py✓ runs
[2, 4, 6]
const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter((n) => n % 2 === 0);
console.log(evens);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
[ 2, 4, 6 ]
import java.util.List;
public class Main {
public static void main(String[] args) {
var nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evens = nums.stream().filter(n -> n % 2 == 0).toList();
System.out.println(evens);
}
}
$ javac Main.java && java Main✓ runs
[2, 4, 6]
List<int> nums = [1, 2, 3, 4, 5, 6];
var evens = nums.Where(n => n % 2 == 0).ToList();
Console.WriteLine($"[{string.Join(", ", evens)}]");
$ dotnet run✓ runs
[2, 4, 6]
Cno equivalent

C has no closures or iterator chains. The C version is a plain loop over nums[i], so this mix-up of &&i32 and i32 cannot happen.

#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> nums{1, 2, 3, 4, 5, 6};
auto evens = nums | std::views::filter([](auto n) { return n % 2 == 0; });
for (auto n : evens) {
std::cout << n << " ";
}
std::cout << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
2 4 6 
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0369?
error[E0369]: cannot calculate the remainder of `&&{integer}` divided by `{integer}`
 --> src/main.rs:3:53
  |
3 |     let evens: Vec<&i32> = nums.iter().filter(|n| n % 2 == 0).collect();
  |                                                   - ^ - {integer}
  |                                                   |
  |                                                   &&{integer}
  |
help: `%` can be used on `&{integer}` if you dereference the left-hand side
  |
3 |     let evens: Vec<&i32> = nums.iter().filter(|n| *n % 2 == 0).collect();
  |                                                   +
What happens in Go

Go has no filter in its standard library. You would write a loop, where n is just the number.

What happens in Kotlin

Kotlin runs this and prints [2, 4, 6]. n is the number itself, so n % 2 works with no references to follow.

What happens in Python

Python prints [2, 4, 6]. n is the number itself, so n % 2 works with no references to follow.

What happens in TypeScript

Prints [ 2, 4, 6 ]. In TypeScript n is the number itself, so n % 2 works as written.

What happens in Java

This prints [2, 4, 6]. Java unboxes the Integer for % automatically, so there are no layers of references to think about.

What happens in C#

LINQ lambdas get the item itself, so n % 2 works and prints [2, 4, 6]. Rust's filter hands you a reference to a reference, which is why the same line fails there.

What happens in C

C has no closures or iterator chains. The C version is a plain loop over nums[i], so this mix-up of &&i32 and i32 cannot happen.

What happens in C++

The lambda takes auto n, a copy of each int, so n % 2 works and this prints 2 4 6. Rust's filter lends the item instead of copying it, so the closure gets &&i32 and you take the &s off yourself.

What Rust sees

Here n is &&i32, a reference to a reference to a number. % works on a number or on one reference to a number, but not through two.

Why Rust says no

Method calls like n.abs() follow references automatically, any number of them. Arithmetic follows one &, and comparisons want the same type on both sides. So this only bites in closures that do arithmetic or comparisons on a &&. The toolkit below explains where the second & comes from, and how to avoid it.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = nums.iter().filter(|&&n| n % 2 == 0).collect();
println!("{evens:?}");
}
$ cargo run
[2, 4, 6]

The toolkit

What an iterator is

An iterator is anything with a next() method that returns Option<Item>: Some(item) while there are items, then None. That’s the whole contract. Every method in this lesson is built on calling next().

Iterators are lazy. Building a chain only describes the work; something at the end (collect, sum, a for loop) has to pull items through. The closure below prints when it runs:

Rustsrc/main.rsRun ↗
fn main() {
let nums = vec![1, 2, 3];
// Build a chain. The closure prints, so we can see when it runs.
let doubled = nums.iter().map(|n| {
println!(" map sees {n}");
n * 2
});
println!("chain built, nothing has run yet");
// collect pulls every item through the chain.
let result: Vec<i32> = doubled.collect();
println!("collected: {result:?}");
// Under the hood, everything is calls to next().
let mut it = nums.iter();
println!("next: {:?}", it.next());
println!("next: {:?}", it.next());
println!("next: {:?}", it.next());
println!("next: {:?}", it.next()); // used up: None from now on
}
$ cargo run
chain built, nothing has run yet
  map sees 1
  map sees 2
  map sees 3
collected: [2, 4, 6]
next: Some(1)
next: Some(2)
next: Some(3)
next: None

“Chain built” comes first; nothing ran until collect asked. A chain you never consume gets a compiler warning (“unused Map that must be used”).

This is Java Streams, C# LINQ, Kotlin sequences and Python generators. It is not JavaScript’s array.map().filter() or Kotlin’s list.map {}, which build a whole new list at every step.

Closures in two minutes

Almost every iterator method takes a closure: a small function written inline, as |parameters| body. It’s the same idea as x => x * 2 in TypeScript, lambda x: x * 2 in Python, { x -> x * 2 } in Kotlin, x -> x * 2 in Java and C#, or a func literal in Go.

Rustsrc/main.rsRun ↗
fn main() {
// A closure is a small function you write inline: |parameters| body.
let double = |x: i32| x * 2;
println!("double(21): {}", double(21));
// Types are usually inferred, and a body can be a block.
let describe = |n| {
if n % 2 == 0 { "even" } else { "odd" }
};
println!("describe(7): {}", describe(7));
// Unlike a plain function, a closure can use variables around it.
let limit = 10;
let over = |x: i32| x > limit;
println!("over(12): {}", over(12));
// Iterator methods take closures: "do this to each item".
let tripled: Vec<i32> = vec![1, 2, 3].into_iter().map(|x| x * 3).collect();
println!("tripled: {tripled:?}");
}
$ cargo run
double(21): 42
describe(7): odd
over(12): true
tripled: [3, 6, 9]

Types are usually inferred, a closure can use the variables around it (limit above), and |x| x * 3 reads as “for each item, give me three times it”. The closures lesson covers the rest.

Why iterators hand you references

This trips up everyone at first. When you write v.iter(), the items are references (&T), not copies of the values. Here’s why, and what to do when you’d rather have values.

Borrowing is the default because it costs nothing

A reference is the address of a value that already exists, plus the compiler’s promise that it stays valid while you use it. On a 64-bit machine it is 8 bytes, whatever it points at. Looking through it copies nothing and allocates nothing, and the collection is still yours when the loop ends.

The alternative is to copy every item. For numbers that’s harmless. For anything that owns heap data (a String, a Vec, a struct holding them), a copy allocates new memory and copies the bytes. This photo type prints every time it is copied:

Rustsrc/main.rsRun ↗
// A photo is big: its pixels live on the heap. Cloning one prints, so you can see it.
struct Photo {
name: String,
pixels: Vec<u8>,
}
impl Clone for Photo {
fn clone(&self) -> Self {
println!(
" cloning {}: copying {} bytes",
self.name,
self.pixels.len()
);
Photo {
name: self.name.clone(),
pixels: self.pixels.clone(),
}
}
}
fn main() {
let photos = vec![
Photo {
name: "beach".into(),
pixels: vec![0; 3_000_000],
},
Photo {
name: "city".into(),
pixels: vec![0; 5_000_000],
},
Photo {
name: "dog".into(),
pixels: vec![0; 2_000_000],
},
];
println!("iter(): borrow each photo");
let total: usize = photos.iter().map(|p| p.pixels.len()).sum();
println!(" total size: {total} bytes, and nothing was copied");
println!(" photos is still usable: {} photos", photos.len());
println!(
" each item handed out: {} bytes (a &Photo), not {} (a Photo) plus its pixels",
size_of::<&Photo>(),
size_of::<Photo>()
);
println!("cloned(): an owned copy of each");
let copies: Vec<Photo> = photos.iter().cloned().collect();
println!(" made {} copies", copies.len());
println!("into_iter(): take the photos themselves, no copies");
let names: Vec<String> = photos.into_iter().map(|p| p.name).collect();
println!(" {names:?}");
}
$ cargo run
iter(): borrow each photo
  total size: 10000000 bytes, and nothing was copied
  photos is still usable: 3 photos
  each item handed out: 8 bytes (a &Photo), not 48 (a Photo) plus its pixels
cloned(): an owned copy of each
  cloning beach: copying 3000000 bytes
  cloning city: copying 5000000 bytes
  cloning dog: copying 2000000 bytes
  made 3 copies
into_iter(): take the photos themselves, no copies
  ["beach", "city", "dog"]
  • iter() read all 10 MB of pixels and copied none of them. Each item was an 8-byte reference.
  • cloned() did the same job with real copies, 10 MB of them. Sometimes that’s what you need; it should never happen by accident.
  • into_iter() handed over the photos themselves. No copies, but photos is gone afterwards: the Vec was used up.

Here is what an item from iter() is, in memory:

let first: &String = names.iter().next().unwrap();
STACKHEAPnames: Vec<String>ptrlen 3cap 3first: &Stringaddresspoints at the first String inside the Vecthe Vec's buffer: 3 StringsStringStringStringtextanatextbotextcy
The item is an address into the Vec's own buffer. Nothing new was allocated, and the text wasn't touched.
Rustsrc/main.rsRun ↗
fn main() {
let names = vec![String::from("ana"), String::from("bo"), String::from("cy")];
let first: &String = names.iter().next().unwrap();
// The item points at the String inside the Vec. It is not a copy.
println!("points into the Vec: {}", std::ptr::eq(first, &names[0]));
println!("a &String is {} bytes", size_of::<&String>());
println!(
"a String is {} bytes, plus its text on the heap",
size_of::<String>()
);
}
$ cargo run
points into the Vec: true
a &String is 8 bytes
a String is 24 bytes, plus its text on the heap

Garbage-collected languages do the same without showing it: a Java for (String s : list) or a Python for s in names gives you a reference to the object already in the list. Rust shows the & because it must know, at compile time, that names outlives every reference into it. That’s what makes the borrowing rules work.

Where && comes from

Each method decides how it hands items to your closure, and the compiler tells you what it chose:

Rustsrc/main.rsRun ↗
use std::any::type_name_of_val as type_of;
fn main() {
let nums = vec![10, 20, 30];
// Each line asks the compiler for the real type of what the closure receives.
let first = nums.iter().next().unwrap();
println!("iter() hands out {}", type_of(&first));
let _ = nums
.iter()
.map(|n| println!("map's closure gets {}", type_of(&n)))
.next();
let _ = nums
.iter()
.filter(|n| {
println!("filter's closure gets {}", type_of(&n));
true
})
.next();
let _ = nums
.iter()
.copied()
.filter(|n| {
println!("after copied(), filter gets {}", type_of(&n));
true
})
.next();
let _ = nums
.clone()
.into_iter()
.map(|n| println!("into_iter(), map gets {}", type_of(&n)))
.next();
}
$ cargo run
iter() hands out           &i32
map's closure gets         &i32
filter's closure gets      &&i32
after copied(), filter gets &i32
into_iter(), map gets      i32
  • iter() lends you each item: &i32.
  • map receives the item and turns it into something else, so your closure gets the item as it is: still &i32.
  • filter is different. It looks at an item and then passes the same item on if you say yes. It can’t give the item away and still have it to pass on, so it lends it to your closure: a reference to the item. The item was already a reference, so your closure gets a reference to a reference, &&i32.

The same goes for the other methods that only look at an item before deciding what to do with it: find, take_while, skip_while and peekable’s next_if. Methods that use the item up (map, filter_map, for_each, fold, any, all) get it with one & less.

&&i32 isn’t a different number. It’s two hops to the same i32: an address of an address. Rust usually follows the hops for you. Sometimes it doesn’t:

Rustsrc/main.rs✗ won't compile
fn main() {
let nums = vec![1, 2, 3, 4];
let evens: Vec<&i32> = nums.iter().filter(|n| n % 2 == 0).collect();
println!("{evens:?}");
}
$ cargo buildWhat is E0369?
error[E0369]: cannot calculate the remainder of `&&{integer}` divided by `{integer}`
 --> src/main.rs:3:53
  |
3 |     let evens: Vec<&i32> = nums.iter().filter(|n| n % 2 == 0).collect();
  |                                                   - ^ - {integer}
  |                                                   |
  |                                                   &&{integer}
  |
help: `%` can be used on `&{integer}` if you dereference the left-hand side
  |
3 |     let evens: Vec<&i32> = nums.iter().filter(|n| *n % 2 == 0).collect();
  |                                                   +

Using a value through references

What follows references for you, and what doesn’t:

Rustsrc/main.rsRun ↗
fn main() {
let nums = vec![3, 8, 12];
let n: &i32 = &nums[1];
// Arithmetic works through one &: Rust defines `&i32 * i32` and friends.
println!("n * 2: {}", n * 2);
println!("n % 3: {}", n % 3);
// Method calls follow references automatically, however many there are.
println!("n.pow(2): {}", n.pow(2));
let nn: &&i32 = &n;
println!("nn.is_positive(): {}", nn.is_positive());
// Comparisons need the same type on both sides.
println!("n == &8: {}", n == &8);
println!("*n == 8: {}", *n == 8);
// * follows a reference to the value. For an i32, that's a copy of the number.
let value: i32 = *n;
println!("*n: {value}");
println!("**nn: {}", **nn);
// A pattern in a closure's parameter does the same: |&&x| takes both & off.
let big: Vec<&i32> = nums.iter().filter(|&&x| x > 5).collect();
println!("filter(|&&x| x > 5): {big:?}");
}
$ cargo run
n * 2: 16
n % 3: 2
n.pow(2): 64
nn.is_positive(): true
n == &8: true
*n == 8: true
*n: 8
**nn: 8
filter(|&&x| x > 5): [8, 12]
  • Method calls follow any number of & automatically: nn.is_positive() worked on a &&i32.
  • Arithmetic works through one &: Rust defines &i32 * i32, &i32 % i32 and the rest. Through two, it doesn’t, which is the error above.
  • Comparisons want the same type on both sides: n == &8 or *n == 8, not n == 8.
  • * follows one reference and gives you the value. For a number, that’s a copy, which is free.
  • A pattern in the parameter does the same: |&x| takes off one &, |&&x| takes off two. You’ll see .filter(|&&n| n > 5) everywhere in Rust; it means “give me the plain number”.

The pattern trick only works for cheap-to-copy values like numbers (types that are Copy). For a String, |&s| would move the String out of a collection you only borrowed, and the compiler refuses:

Rustsrc/main.rs✗ won't compile
fn main() {
let names = vec![String::from("ana"), String::from("bo")];
// |&s| tries to take the String out from behind the reference.
let copies: Vec<String> = names.iter().map(|&s| s).collect();
println!("{copies:?}");
}
$ cargo buildWhat is E0507?
error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:49
  |
4 |     let copies: Vec<String> = names.iter().map(|&s| s).collect();
  |                                                 ^-
  |                                                  |
  |                                                  data moved here because `s` has type `String`, which does not implement the `Copy` trait
  |
help: consider removing the borrow
  |
4 -     let copies: Vec<String> = names.iter().map(|&s| s).collect();
4 +     let copies: Vec<String> = names.iter().map(|s| s).collect();

Going without references

To work with plain values, say so before the rest of the chain:

You write Your closures get What it costs Afterwards
.iter() &T nothing collection unchanged
.iter().copied() T copies each value: free for numbers, char, bool collection unchanged
.iter().cloned() T a full copy of each item, allocations included collection unchanged
.into_iter() T nothing: the items are moved out collection used up
.iter_mut() &mut T nothing items changed in place
Rustsrc/main.rsRun ↗
fn main() {
let nums = vec![1, 2, 3, 4];
// Parameter types are written out here, so the compiler checks every claim.
// .iter() hands out &i32, and map gets each item as it is.
let a: Vec<i32> = nums.iter().map(|n: &i32| n * 10).collect();
// filter only looks, so it borrows the item: a & on top of &i32.
let b: Vec<&i32> = nums.iter().filter(|n: &&i32| **n > 2).collect();
println!("iter(): map {a:?}, filter {b:?}");
// A pattern in the parameter peels the & off for you.
let c: Vec<&i32> = nums.iter().filter(|&&n| n > 2).collect();
println!("|&&n| pattern: {c:?}");
// copied() turns &i32 into i32 first. Then there is one & less everywhere.
let d: Vec<i32> = nums.iter().copied().filter(|n: &i32| *n > 2).collect();
let e: Vec<i32> = nums.iter().copied().filter(|&n| n > 2).collect();
println!("copied(): {d:?} {e:?}");
// For types that aren't Copy, cloned() makes owned copies.
let names = vec![String::from("ana"), String::from("bo")];
let owned: Vec<String> = names.iter().filter(|s| s.len() > 2).cloned().collect();
println!("cloned(): {owned:?}, names still here: {names:?}");
// into_iter() on the Vec itself hands out the items, so there is nothing
// to copy, but `names` is used up.
let shouted: Vec<String> = names
.into_iter()
.map(|s: String| s.to_uppercase())
.collect();
println!("into_iter(): {shouted:?}");
}
$ cargo run
iter(): map [10, 20, 30, 40], filter [3, 4]
|&&n| pattern: [3, 4]
copied(): [3, 4] [3, 4]
cloned(): ["ana"], names still here: ["ana", "bo"]
into_iter(): ["ANA", "BO"]

into_iter() is the cheapest way to own the items, but only if you’re finished with the collection:

Rustsrc/main.rs✗ won't compile
fn main() {
let names = vec![String::from("ana"), String::from("bo")];
let upper: Vec<String> = names.into_iter().map(|s| s.to_uppercase()).collect();
println!("{upper:?}");
println!("{names:?}"); // into_iter() took the Vec
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `names`
 --> src/main.rs:5:16
  |
2 |     let names = vec![String::from("ana"), String::from("bo")];
  |         ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait
3 |     let upper: Vec<String> = names.into_iter().map(|s| s.to_uppercase()).collect();
  |                                    ----------- `names` moved due to this method call
4 |     println!("{upper:?}");
5 |     println!("{names:?}"); // into_iter() took the Vec
  |                ^^^^^ value borrowed here after move
  |
help: you can `clone` the value and consume it, but this might not be your desired behavior
  |
3 |     let upper: Vec<String> = names.clone().into_iter().map(|s| s.to_uppercase()).collect();
  |                                   ++++++++

  ... (notes pointing into the standard library trimmed)

The rules of thumb:

  • Numbers and other small values: start with .iter().copied() and forget the &s.
  • Strings and anything with heap data: keep the references while you filter and search, and call .cloned() at the end, on just the items you keep.
  • Done with the collection? into_iter() owns the items with no copying.
  • Need to change items? iter_mut(); Looping over a collection covers it.

Translating from what you know

The methods you already use, and their Rust names. This shows the language you picked at the top of the page.

Where iterators come from

From a collection you pick iter() (items are &T), iter_mut() (&mut T) or into_iter() (T, and the collection is used up). These match for x in &v, for x in &mut v and for x in v (Looping over a collection). Most iterators come from elsewhere:

Rustsrc/main.rsRun ↗
use std::collections::BTreeMap;
use std::iter;
fn main() {
// Ranges are iterators already.
let up: Vec<i32> = (1..5).collect();
let up_to: Vec<i32> = (1..=5).collect();
println!("1..5: {up:?} 1..=5: {up_to:?}");
// Text: see the strings lesson for more.
let text = "one two\nthree";
println!("chars: {:?}", "héllo".chars().collect::<Vec<_>>());
println!("lines: {:?}", text.lines().collect::<Vec<_>>());
println!("split: {:?}", "a,b,,c".split(',').collect::<Vec<_>>());
// Maps give (key, value) pairs. BTreeMap keeps keys sorted;
// a HashMap gives them in an order that changes from run to run.
let ages = BTreeMap::from([("bo", 31), ("ana", 28)]);
for (name, age) in &ages {
println!("map: {name} is {age}");
}
println!("keys: {:?}", ages.keys().collect::<Vec<_>>());
println!("values: {:?}", ages.values().collect::<Vec<_>>());
// Making iterators from nothing.
let dashes: String = iter::repeat('-').take(5).collect();
println!("repeat: {dashes}");
println!(
"repeat_n: {:?}",
iter::repeat_n("hi", 3).collect::<Vec<_>>()
);
println!("once: {:?}", iter::once(42).collect::<Vec<_>>());
// successors: each item is computed from the previous one.
// checked_mul returns None on overflow, which ends the sequence.
let powers: Vec<u16> = iter::successors(Some(1u16), |&n| n.checked_mul(10)).collect();
println!("successors: {powers:?}");
// from_fn: a closure that returns Some(item) or None to stop.
let mut count = 0;
let counter = iter::from_fn(|| {
count += 1;
if count <= 3 { Some(count) } else { None }
});
println!("from_fn: {:?}", counter.collect::<Vec<_>>());
}
$ cargo run
1..5: [1, 2, 3, 4]   1..=5: [1, 2, 3, 4, 5]
chars: ['h', 'é', 'l', 'l', 'o']
lines: ["one two", "three"]
split: ["a", "b", "", "c"]
map: ana is 28
map: bo is 31
keys: ["ana", "bo"]
values: [28, 31]
repeat: -----
repeat_n: ["hi", "hi", "hi"]
once: [42]
successors: [1, 10, 100, 1000, 10000]
from_fn: [1, 2, 3]

Ranges, text (strings lesson) and maps are all iterators. A HashMap’s order changes between runs, on purpose; sort, or use a BTreeMap. std::iter builds iterators from nothing: repeat, repeat_n, once, successors (each item from the one before) and from_fn (a closure returning Some until None).

Adaptors

Methods that take an iterator and give back another are adaptors, and they’re lazy. Results below are what collect() would give.

Method What it does Example → result
map(f), filter(f) change each item; keep the accepted ones [1, 2, 3], square → [1, 4, 9]
filter_map(f) both in one: the closure returns Option ["7", "x", "12"], s.parse().ok() → [7, 12]
flat_map(f), flatten() one item becomes several; remove a level of nesting ["hi", "yo"], w.chars() → ['h', 'i', 'y', 'o']
flatten() on Options an Option is zero or one items [Some(1), None, Some(3)] → [1, 3]
take(n), skip(n) the first n; all but the first n [3, 8, 1, 9, 4, 7, 2]: 3 → [3, 8, 1]; 5 → [7, 2]
take_while(f), skip_while(f) stop; start, at the first failure same list, < 9 → [3, 8, 1]; [9, 4, 7, 2]
step_by(n), rev() every n-th item; backwards (0..10).step_by(3) → [0, 3, 6, 9]
enumerate() (index, item) pairs ["ana", "bo"] → [(0, "ana"), (1, "bo")]
zip(other) side by side, until the shorter ends (no error) ["ana", "bo"], [90, 75, 82] → [("ana", 90), ("bo", 75)]
chain(other), cycle() one then another; repeat forever (take must stop it) (1..=3).chain(7..=8) → [1, 2, 3, 7, 8]
  • take_while stops at the first failure, even though 4, 7 and 2 would pass. It’s for ordered data; for every passing item, use filter.
  • rev needs an iterator that can run from the back (a DoubleEndedIterator): slices, ranges and chars() can, from_fn can’t.

Peeking, windows and chunks

Rustsrc/main.rsRun ↗
fn main() {
// peekable lets you look at the next item without taking it.
// Here: split "aaabccdd" into runs of the same letter.
let mut chars = "aaabccdd".chars().peekable();
let mut runs = Vec::new();
while let Some(c) = chars.next() {
let mut len = 1;
while chars.peek() == Some(&c) {
chars.next();
len += 1;
}
runs.push(format!("{c}{len}"));
}
println!("runs: {}", runs.join(" "));
// next_if takes the next item only if it passes the test.
let mut it = "123abc".chars().peekable();
let mut digits = String::new();
while let Some(d) = it.next_if(|c| c.is_ascii_digit()) {
digits.push(d);
}
println!("digits: {digits}, left over: {}", it.collect::<String>());
// windows and chunks are methods on slices (and so on Vec and arrays),
// not on iterators. They hand out sub-slices without copying.
let temps = [12, 14, 13, 17, 21];
let rises: Vec<i32> = temps.windows(2).map(|w| w[1] - w[0]).collect();
println!("windows(2) differences: {rises:?}");
for chunk in temps.chunks(2) {
println!("chunks(2): {chunk:?}"); // the last chunk can be shorter
}
println!(
"chunks_exact(2) remainder: {:?}",
temps.chunks_exact(2).remainder()
);
}
$ cargo run
runs: a3 b1 c2 d2
digits: 123, left over: abc
windows(2) differences: [2, -1, 4, 4]
chunks(2): [12, 14]
chunks(2): [13, 17]
chunks(2): [21]
chunks_exact(2) remainder: [21]

peekable() lets you peek() at the next item without taking it, which tokenizers need; next_if takes it only if it passes a test.

windows(n) and chunks(n) are slice methods, not iterator methods, so collect an iterator first. windows(2) gives overlapping pairs; chunks(2) gives pieces that don’t overlap, the last possibly shorter (chunks_exact keeps the leftovers in remainder()).

Watching a chain, and running totals

Rustsrc/main.rsRun ↗
fn main() {
// inspect runs a closure on each item as it passes, and changes nothing.
// It is for debugging a chain without taking it apart.
let total: i32 = [1, 2, 3, 4]
.iter()
.inspect(|n| println!(" before filter: {n}"))
.filter(|&&n| n % 2 == 0)
.inspect(|n| println!(" after filter: {n}"))
.sum();
println!("total: {total}");
// scan is map with a running state: here, a running balance.
let changes = [100, -30, -50, 20];
let balances: Vec<i32> = changes
.iter()
.scan(0, |balance, change| {
*balance += change;
Some(*balance) // return None to stop early
})
.collect();
println!("scan balances: {balances:?}");
}
$ cargo run
  before filter: 1
  before filter: 2
  after filter: 2
  before filter: 3
  before filter: 4
  after filter: 4
total: 6
scan balances: [100, 70, 20, 40]

inspect is a debug println! for a chain. It shows the real order: each item goes through the whole chain before the next starts, as in Java Streams and C# LINQ but not JavaScript’s array methods.

scan is map with memory: the closure changes a state (here, the balance) and returns a value for each item, or None to stop.

Consumers: collect

Consumers pull every item through and produce one result. collect builds whatever collection you name, with a let annotation or the turbofish ::<>. Write the item type as _ (Vec<_>) and Rust works it out. Collecting chars or &strs into a String glues them together.

Rustsrc/main.rsRun ↗
use std::collections::{BTreeSet, HashMap, HashSet};
fn main() {
let words = ["apple", "bob", "apple", "cat"];
// The type you ask for decides what collect builds.
let v: Vec<&str> = words.iter().copied().collect();
println!("Vec: {v:?}");
let s: String = words.iter().map(|w| w.chars().next().unwrap()).collect();
println!("String from chars: {s}");
let joined: String = words.iter().copied().collect();
println!("String from &strs: {joined}");
// A set drops duplicates. HashSet order is random, so we only print the length.
let unique: HashSet<&str> = words.iter().copied().collect();
println!("HashSet len: {}", unique.len());
let sorted: BTreeSet<&str> = words.iter().copied().collect();
println!("BTreeSet: {sorted:?}");
// Pairs collect into a map. A repeated key keeps the last value.
let lengths: HashMap<&str, usize> = words.iter().map(|w| (*w, w.len())).collect();
println!("HashMap[\"apple\"]: {}", lengths["apple"]);
// The turbofish ::<> says the same thing without a `let` type.
let n = (1..=3).collect::<Vec<i32>>();
// `_` lets Rust work out the item type: you only name the container.
let m: Vec<_> = (1..=3).map(|x| x * 10).collect();
println!("turbofish: {n:?}, with _: {m:?}");
}
$ cargo run
Vec: ["apple", "bob", "apple", "cat"]
String from chars: abac
String from &strs: applebobapplecat
HashSet len: 3
BTreeSet: {"apple", "bob", "cat"}
HashMap["apple"]: 5
turbofish: [1, 2, 3], with _: [10, 20, 30]

Collecting into a Result: stop at the first error

This is the one people from other languages don’t expect.

Rustsrc/main.rsRun ↗
fn main() {
let good = ["1", "2", "3"];
let bad = ["1", "two", "3", "four"];
// Each parse gives a Result. Collecting into Vec<Result<..>> keeps them all.
let each: Vec<Result<i32, _>> = bad.iter().map(|s| s.parse::<i32>()).collect();
println!("Vec<Result>: {each:?}");
// Collecting into Result<Vec<..>, ..> flips it: Ok with every value,
// or the first Err.
let all: Result<Vec<i32>, _> = good.iter().map(|s| s.parse::<i32>()).collect();
println!("good -> {all:?}");
// It stops at the first Err. inspect shows "3" and "four" are never parsed.
let all: Result<Vec<i32>, _> = bad
.iter()
.inspect(|s| println!(" parsing {s:?}"))
.map(|s| s.parse::<i32>())
.collect();
println!("bad -> {all:?}");
// Option works the same way: Some(Vec) only if every item is Some.
let firsts: Option<Vec<char>> = ["ab", "cd"].iter().map(|s| s.chars().next()).collect();
println!("all Some -> {firsts:?}");
let firsts: Option<Vec<char>> = ["ab", ""].iter().map(|s| s.chars().next()).collect();
println!("one None -> {firsts:?}");
}
$ cargo run
Vec<Result>: [Ok(1), Err(ParseIntError { kind: InvalidDigit }), Ok(3), Err(ParseIntError { kind: InvalidDigit })]
good -> Ok([1, 2, 3])
  parsing "1"
  parsing "two"
bad  -> Err(ParseIntError { kind: InvalidDigit })
all Some -> Some(['a', 'c'])
one None -> None

Collect items that are Results into Result<Vec<T>, E> instead of Vec<Result<T, E>>. You get Ok with all the values, or the first Err, and it stops there: inspect shows that "3" and "four" are never parsed. Option works the same way.

It’s the iterator version of ?: “parse every line, or tell me what was wrong” in one expression. The errors lesson covers Result and ?.

Consumers: sum, product, count

sum looks like it needs no help. It does:

Rustsrc/main.rs✗ won't compile
fn main() {
let prices = [3, 4, 5];
let total = prices.iter().sum();
println!("{total}");
}
$ cargo buildWhat is E0283?
error[E0283]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let total = prices.iter().sum();
  |         ^^^^^                 --- type must be known at this point
  |
  = note: the type must implement `Sum<&i32>`
  = help: the following types implement trait `Sum<A>`:
            `Duration` implements `Sum<&'a Duration>`
            `Duration` implements `Sum`
            `Option<T>` implements `Sum<Option<U>>`
            `Result<T, E>` implements `Sum<Result<U, E>>`
            `Saturating<u128>` implements `Sum<&'a Saturating<u128>>`
            `Saturating<u128>` implements `Sum`
            `Saturating<u16>` implements `Sum<&'a Saturating<u16>>`
            `Saturating<u16>` implements `Sum`
          and 88 others
help: consider giving `total` an explicit type
  |
3 |     let total: /* Type */ = prices.iter().sum();
  |              ++++++++++++

  ... (notes pointing into the standard library trimmed)

sum can produce many types (i32, i64, Option, Result, Duration), and the compiler won’t guess:

Rustsrc/main.rsRun ↗
fn main() {
let prices = [3, 4, 5];
// sum and product can produce several types, so name the one you want.
let total: i32 = prices.iter().sum();
let product = prices.iter().product::<i32>();
println!("sum: {total}, product: {product}");
// An empty sum is 0 and an empty product is 1, not None.
let none: [i32; 0] = [];
println!("empty sum: {}", none.iter().sum::<i32>());
println!("empty product: {}", none.iter().product::<i32>());
// Floats work too.
let avg = [1.5, 2.5].iter().sum::<f64>() / 2.0;
println!("average: {avg}");
// count walks the whole iterator.
println!(
"count of evens: {}",
(1..=10).filter(|n| n % 2 == 0).count()
);
}
$ cargo run
sum: 12, product: 60
empty sum: 0
empty product: 1
average: 2
count of evens: 5

An empty sum is 0 and an empty product 1, not None, so check for empty input before dividing. (average prints 2 because that is how {} prints a whole-number f64.)

Consumers: min and max

Rustsrc/main.rsRun ↗
fn main() {
let nums = [4, 9, 2, 9, 1];
// Option, because an empty iterator has no max.
println!("max: {:?}, min: {:?}", nums.iter().max(), nums.iter().min());
let empty: [i32; 0] = [];
println!("max of empty: {:?}", empty.iter().max());
// _by_key: compare by something computed from each item.
let words = ["kiwi", "fig", "banana", "plum"];
println!("longest: {:?}", words.iter().max_by_key(|w| w.len()));
println!("shortest: {:?}", words.iter().min_by_key(|w| w.len()));
// On a tie, max_ returns the last of the equals and min_ the first.
println!(
"max_by_key tie (kiwi, plum): {:?}",
["kiwi", "plum"].iter().max_by_key(|w| w.len())
);
// f64 is not Ord (NaN isn't bigger or smaller than anything),
// so max() won't compile. total_cmp gives floats a full order.
let temps: [f64; 3] = [21.5, 19.0, 23.25];
let hottest = temps.iter().copied().max_by(|a, b| a.total_cmp(b));
println!("hottest: {hottest:?}");
// Or fold with f64::min (or f64::max), which ignore NaN.
let coldest = temps.iter().copied().fold(f64::INFINITY, f64::min);
println!("coldest: {coldest}");
// Floats by key: compare with total_cmp inside max_by.
let cities: [(&str, f64); 3] = [("Oslo", 4.5), ("Lima", 19.0), ("Rome", 15.5)];
let warmest = cities.iter().max_by(|a, b| a.1.total_cmp(&b.1));
println!("warmest city: {warmest:?}");
}
$ cargo run
max: Some(9), min: Some(1)
max of empty: None
longest: Some("banana")
shortest: Some("fig")
max_by_key tie (kiwi, plum): Some("plum")
hottest: Some(23.25)
coldest: 19
warmest city: Some(("Lima", 19.0))

min and max return Option, because an empty iterator has neither. max_by_key and min_by_key compare by something you compute and return the whole item. On a tie, max_by_key returns the last of the equal items and min_by_key the first. That’s documented, and it matters when you expect “the first longest word”.

Floats are the catch. max() needs Ord (any two items can be compared), and f64 isn’t, because NaN is neither bigger nor smaller than anything:

Rustsrc/main.rs✗ won't compile
fn main() {
let temps: [f64; 3] = [21.5, 19.0, 23.25];
let hottest = temps.iter().max();
println!("{hottest:?}");
}
$ cargo buildWhat is E0277?
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`
  |
  = help: the following other types implement trait `Ord`:
            i128
            i16
            i32
            i64
            i8
            isize
            u128
            u16
          and 4 others
  = note: required for `&f64` to implement `Ord`
  ... (notes pointing into the standard library trimmed)

The fix is total_cmp, which gives floats a complete order (with NaN at the ends), used through max_by or min_by. For plain numbers, fold(f64::INFINITY, f64::min) also works. Other languages make the choice for you, and not consistently. On the list [1, NaN, 3], run on this site’s runner (Java 25, .NET 10): Java’s DoubleStream.max() gives NaN, but Stream.min(Comparator.naturalOrder()) gives 1.0; C#’s Max() gives 3 while Min() gives NaN. Rust makes you pick, and the pick is visible in the code.

Consumers: searching and folding

Method What it does Example → result
any(f), all(f) stop as soon as the answer is known on [3, 8, 12, 5, 20], > 10 → true; > 0 → true
find(f), position(f) the first match; its index same list, > 10 → Some(12); Some(2)
find_map(f) filter_map that stops at the first Some l.strip_prefix("port=") → Some("8080")
last(), nth(n) one item; nth counts from 0 and uses up the ones before nth(2) on [3, 8, 1, 9] → Some(1)
fold(init, f), reduce(f) combine all items into one value; reduce starts with the first item, so None if empty squares of 1..=6, added → 91
partition(f), unzip() split into two collections: by a test; from pairs 1..=6, even or not → [2, 4, 6] and [1, 3, 5]
Rustsrc/main.rsRun ↗
fn main() {
let nums = [3, 8, 12, 5, 20];
println!("any > 10: {}", nums.iter().any(|&n| n > 10));
println!("all > 0: {}", nums.iter().all(|&n| n > 0));
// On an empty iterator, any is false and all is true.
let empty: [i32; 0] = [];
println!(
"empty any: {}, empty all: {}",
empty.iter().any(|&n| n > 0),
empty.iter().all(|&n| n > 0)
);
// find returns the first match; position returns its index.
println!("find > 10: {:?}", nums.iter().find(|&&n| n > 10));
println!("position > 10: {:?}", nums.iter().position(|&n| n > 10));
println!("find > 100: {:?}", nums.iter().find(|&&n| n > 100));
println!("rposition > 10: {:?}", nums.iter().rposition(|&n| n > 10));
// find_map: the first item for which the closure returns Some.
let lines = ["# comment", "port=8080", "host=x"];
let port = lines.iter().find_map(|l| l.strip_prefix("port="));
println!("find_map port: {port:?}");
// contains is on slices, not iterators.
println!("contains(&12): {}", nums.contains(&12));
}
$ cargo run
any > 10: true
all > 0: true
empty any: false, empty all: true
find > 10: Some(12)
position > 10: Some(2)
find > 100: None
rposition > 10: Some(4)
find_map port: Some("8080")
contains(&12): true

On an empty iterator, any is false and all is true (“every one of none of them”), as in Python and Java. contains is a slice method; on an iterator, use any(|x| x == &wanted).

Rustsrc/main.rsRun ↗
fn main() {
let nums = [1, 2, 3, 4, 5, 6];
// fold: start with a value and combine every item into it.
let sum_of_squares = nums.iter().fold(0, |acc, n| acc + n * n);
println!("fold sum of squares: {sum_of_squares}");
let csv = nums.iter().fold(String::new(), |mut acc, n| {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(&n.to_string());
acc
});
println!("fold into a String: {csv}");
// reduce: like fold, but the first item is the start. None if empty.
println!("reduce: {:?}", nums.iter().copied().reduce(|a, b| a * b));
// partition: split into two collections by a test.
let (even, odd): (Vec<i32>, Vec<i32>) = nums.iter().partition(|&&n| n % 2 == 0);
println!("partition: even {even:?}, odd {odd:?}");
// unzip: a list of pairs into a pair of lists.
let pairs = [("ana", 28), ("bo", 31)];
let (names, ages): (Vec<&str>, Vec<i32>) = pairs.iter().copied().unzip();
println!("unzip: {names:?} {ages:?}");
// for_each: run a closure on every item. Same as a for loop;
// handy at the end of a long chain.
nums.iter()
.filter(|&&n| n > 4)
.for_each(|n| println!("for_each: {n}"));
}
$ cargo run
fold sum of squares: 91
fold into a String: 1,2,3,4,5,6
reduce: Some(720)
partition: even [2, 4, 6], odd [1, 3, 5]
unzip: ["ana", "bo"] [28, 31]
for_each: 5
for_each: 6

When you’re building a collection, collect, sum or partition usually says it better. partition and unzip need their target types written out. for_each is a for loop at the end of a chain, but only for can break or use ?.

Sorting and removing duplicates

Iterators have no sort: sorting needs every item at once. Collect into a Vec and sort that in place.

  • sort and sort_by_key are stable: equal items keep their order. sort_unstable is a little faster, and fine when equal items look the same.
  • Floats need sort_by with a.total_cmp(b), as with max.
  • dedup removes only repeats that sit next to each other, like uniq. Sort first, or collect into a HashSet.
  • For “highest score first, then by name”, use a tuple key with Reverse: sort_by_key(|&(name, score)| (Reverse(score), name)).

Counting and grouping

The standard library has no group_by or counts on iterators. The usual pattern is a loop with the map’s entry method:

Rustsrc/main.rsRun ↗
use std::collections::{BTreeMap, HashMap};
fn main() {
let text = "the cat and the hat and the bat";
// Counting: entry finds the key or inserts 0, and gives you the value to change.
let mut counts: HashMap<&str, usize> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
// HashMap order is random. Collect and sort to print it in a fixed order.
let mut by_count: Vec<(&str, usize)> = counts.into_iter().collect();
by_count.sort_by_key(|&(word, n)| (std::cmp::Reverse(n), word));
println!("counts: {by_count:?}");
// Grouping: or_default() starts each group with an empty Vec.
// A BTreeMap keeps the keys sorted.
let mut by_len: BTreeMap<usize, Vec<&str>> = BTreeMap::new();
for word in ["fig", "kiwi", "pear", "yam", "plum"] {
by_len.entry(word.len()).or_default().push(word);
}
println!("grouped by length: {by_len:?}");
// Runs of neighbours that belong together: chunk_by on a slice.
// Only neighbours are grouped, like Unix `uniq`.
let readings = [1, 2, 3, 7, 8, 12];
let runs: Vec<&[i32]> = readings.chunk_by(|a, b| b - a == 1).collect();
println!("chunk_by consecutive: {runs:?}");
}
$ cargo run
counts: [("the", 3), ("and", 2), ("bat", 1), ("cat", 1), ("hat", 1)]
grouped by length: {3: ["fig", "yam"], 4: ["kiwi", "pear", "plum"]}
chunk_by consecutive: [[1, 2, 3], [7, 8], [12]]

entry(key) finds the key or makes a space for it. or_insert(0) puts 0 there if it’s new, and either way gives you the value to change; or_default() uses the type’s default (0, or an empty Vec). The maps lesson has more. To group only neighbours, slices have chunk_by, which splits where the test between two neighbours fails.

Going deeper

The Iterator trait, and writing your own

Iterator is a trait: a list of methods a type promises to have. You write next; over 70 more come free, all built on it, so your type gets map, filter, zip, sum and the rest.

Rustsrc/main.rsRun ↗
// Fibonacci numbers, one at a time. The struct holds the state between calls.
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci {
current: 0,
next: 1,
}
}
}
impl Iterator for Fibonacci {
type Item = u64; // what next() hands out
// The only method you have to write.
fn next(&mut self) -> Option<u64> {
let value = self.current;
// checked_add returns None on overflow, which ends the sequence.
let after = self.current.checked_add(self.next)?;
self.current = self.next;
self.next = after;
Some(value)
}
}
fn main() {
// Every adaptor and consumer now works, without writing any of them.
let first: Vec<u64> = Fibonacci::new().take(10).collect();
println!("take(10): {first:?}");
let even_sum: u64 = Fibonacci::new()
.take_while(|&n| n < 4_000_000)
.filter(|n| n % 2 == 0)
.sum();
println!("sum of even ones under 4 million: {even_sum}");
let first_big = Fibonacci::new().position(|n| n > 1000);
println!("position of first > 1000: {first_big:?}");
for (i, n) in Fibonacci::new().enumerate().skip(20).step_by(10) {
println!("fib({i}) = {n}");
}
// It stops by itself before u64 overflows.
println!(
"count (it stops before u64 overflows): {}",
Fibonacci::new().count()
);
}
$ cargo run
take(10): [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
sum of even ones under 4 million: 4613732
position of first > 1000: Some(17)
fib(20) = 6765
fib(30) = 832040
fib(40) = 102334155
fib(50) = 12586269025
fib(60) = 1548008755920
fib(70) = 190392490709135
fib(80) = 23416728348467685
fib(90) = 2880067194370816120
count (it stops before u64 overflows): 92

type Item = u64 says what next hands out; the struct holds what the iterator remembers between calls. The ? after checked_add returns None as soon as an addition would overflow, so the iterator ends by itself. (It ends two numbers early, because it checks the number after the one it returns. Being exact needs a little more state.) An iterator can also go on forever, like repeat and cycle; the consumer then stops it, as take_while does here.

Java’s Iterator and C#’s IEnumerator split this across two calls (hasNext()/next(), MoveNext()/Current); Rust’s Option does both. Python generators and C#’s yield return build the state for you; stable Rust has no yield yet, so keep it in a struct or use iter::from_fn.

What a chain compiles to

A chain like nums.iter().filter(..).map(..) is not a list of steps kept for later. It’s a struct wrapping a struct:

Rustsrc/main.rsRun ↗
use std::any::type_name_of_val;
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
// A chain is a value: a struct wrapping a struct wrapping the slice iterator.
let chain = nums.iter().filter(|&&n| n % 2 == 0).map(|n| n * n);
// type_name is for debugging; its exact text isn't guaranteed between versions.
println!("type: {}", type_name_of_val(&chain));
// The closures capture nothing, so they take no space. What's left is the
// slice iterator's two pointers: no heap allocation, no list of steps.
println!("size: {} bytes", size_of_val(&chain));
let result: Vec<i32> = chain.collect();
println!("result: {result:?}");
}
$ cargo run
type: core::iter::adapters::map::Map<core::iter::adapters::filter::Filter<core::slice::iter::Iter<'_, i32>, main::main::{{closure}}>, main::main::{{closure}}>
size: 16 bytes
result: [4, 16, 36]

Map holds a Filter, which holds the slice iterator. next() on the Map calls it on the Filter, which calls it on the slice iterator until an item passes. The closures here capture nothing, so they take no space, and the whole chain is 16 bytes on the stack: the slice iterator’s two pointers. Nothing is on the heap, and there’s no list of callbacks underneath.

Each closure has its own type, so the compiler knows which code every next() calls. In an optimized build it typically inlines all of it into the loop you’d write by hand, and iterating a slice lets it drop the bounds checks v[i] may need. The Rust Book shows this in Comparing Performance: Loops vs. Iterators, and the Compiler Explorer shows the machine code for your own chain.

Two honest caveats:

  • This is about optimized builds (cargo run --release). In a debug build nothing is inlined, and a long chain can be noticeably slower than a loop.
  • In a Box<dyn Iterator> (to return different iterator types from one function, say), the compiler no longer knows which next() to call, so each item costs an indirect call. Usually you can return impl Iterator<Item = T> instead, which keeps the exact type.

You can’t change a Vec while iterating it

In Java this is usually a ConcurrentModificationException at runtime (the JDK documents the check as best-effort). In Python, appending to a list while you loop over it can loop forever. In C++ it’s undefined behaviour, because push_back may move the elements and leave the iterator pointing at freed memory. In Rust, it doesn’t compile:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut queue = vec![1, 2];
for n in &queue {
if *n < 20 {
queue.push(n * 3);
}
}
println!("{queue:?}");
}
$ cargo buildWhat is E0502?
error[E0502]: cannot borrow `queue` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:13
  |
3 |       for n in &queue {
  |       -        ------
  |       |        |
  |       |        immutable borrow occurs here
  |  _____|        immutable borrow later used here
  | |
4 | |         if *n < 20 {
5 | |             queue.push(n * 3);
  | |             ^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | |         }
7 | |     }
  | |_____- this for loop borrows `queue` immutably, preventing mutation within its body
  |
  = help: consider using an index-based loop instead, or collecting modifications into a separate collection

The loop borrows queue while it runs, and push needs to borrow it mutably. The borrowing rules allow one or the other. The compiler’s hint names the usual fixes:

Rustsrc/main.rsRun ↗
fn main() {
// Fix 1: work out what to add, then add it. The borrow has ended by then.
let mut queue = vec![1, 2];
let extra: Vec<i32> = queue.iter().filter(|&&n| n < 20).map(|n| n * 3).collect();
queue.extend(extra);
println!("collect, then extend: {queue:?}");
// Fix 2: loop by index, when the new items should be visited too.
// queue.len() is read again on every pass, so the loop sees what was pushed.
let mut queue = vec![1, 2];
let mut i = 0;
while i < queue.len() {
if queue[i] < 20 {
queue.push(queue[i] * 3);
}
i += 1;
}
println!("index loop: {queue:?}");
// Changing items in place is fine: iter_mut borrows each one mutably.
let mut prices = vec![10, 20, 30];
for p in prices.iter_mut() {
*p += 1;
}
println!("iter_mut: {prices:?}");
// Removing as you go: retain does it in one pass.
let mut nums = vec![1, 2, 3, 4, 5, 6];
nums.retain(|n| n % 3 != 0);
println!("retain: {nums:?}");
}
$ cargo run
collect, then extend: [1, 2, 3, 6]
index loop: [1, 2, 3, 6, 9, 18, 27, 54]
iter_mut: [11, 21, 31]
retain: [1, 2, 4, 5]
  • Collect first, then change. Work out what to add with one chain, then extend; the borrow has ended by the time you push. It only looks at the items there at the start.
  • Loop by index when new items must be visited too, as in a work queue. queue[i] copies one number out, so no borrow lasts across the push. (A VecDeque with pop_front is cleaner for a real queue.)
  • Changing items is fine with iter_mut(); removing them is retain.

An iterator can only be used once

Rustsrc/main.rs✗ won't compile
fn main() {
let nums = vec![1, 2, 3];
let evens = nums.iter().filter(|&&n| n % 2 == 0);
let how_many = evens.count();
let total: i32 = evens.sum();
println!("{how_many} {total}");
}
$ cargo buildWhat is E0382?
error[E0382]: use of moved value: `evens`
 --> src/main.rs:5:22
  |
3 |     let evens = nums.iter().filter(|&&n| n % 2 == 0);
  |         ----- move occurs because `evens` has type `Filter<std::slice::Iter<'_, i32>, {closure@src/main.rs:3:36: 3:41}>`, which does not implement the `Copy` trait
4 |     let how_many = evens.count();
  |                          ------- `evens` moved due to this method call
5 |     let total: i32 = evens.sum();
  |                      ^^^^^ value used here after move
  |
help: you can `clone` the value and consume it, but this might not be your desired behavior
  |
4 |     let how_many = evens.clone().count();
  |                         ++++++++

  ... (notes pointing into the standard library trimmed)

count takes the iterator by value (self) and uses it up. In Java, reusing a Stream compiles and then throws IllegalStateException. In Python, a second loop over a used generator quietly gives you nothing. Rust tells you at compile time.

Build the chain again (it’s cheap), or collect once into a Vec. .clone(), as the hint suggests, works for most std iterators but runs the whole chain a second time.

A related surprise: take_while has to pull the first failing item to know it failed, and that item is then gone. by_ref() lets you keep using an iterator after an adaptor has borrowed it, and shows this:

Rustsrc/main.rsRun ↗
fn main() {
// take_while has to pull the first failing item to see that it fails.
// That item is then gone.
let mut it = [1, 2, 10, 3, 4].into_iter();
let small: Vec<i32> = it.by_ref().take_while(|&n| n < 5).collect();
let rest: Vec<i32> = it.collect();
println!("take_while: {small:?}, rest: {rest:?}"); // the 10 is lost
// peekable + next_if looks first and leaves the failing item in place.
let mut it = [1, 2, 10, 3, 4].into_iter().peekable();
let mut small = Vec::new();
while let Some(n) = it.next_if(|&n| n < 5) {
small.push(n);
}
let rest: Vec<i32> = it.collect();
println!("next_if: {small:?}, rest: {rest:?}");
}
$ cargo run
take_while: [1, 2], rest: [3, 4]
next_if: [1, 2], rest: [10, 3, 4]

When that item matters (in a parser, usually), use peekable and next_if.

IntoIterator: why for x in v works

A for loop needs something that can become an iterator: a type that implements IntoIterator. for x in v calls IntoIterator::into_iter(v), then next() until None.

Rustsrc/main.rsRun ↗
use std::collections::BTreeSet;
// Accepts anything a `for` loop accepts, as long as it yields i32.
fn total(items: impl IntoIterator<Item = i32>) -> i32 {
let mut sum = 0;
for n in items {
sum += n;
}
sum
}
fn main() {
println!("Vec: {}", total(vec![1, 2, 3]));
println!("array: {}", total([4, 5]));
println!("range: {}", total(1..=10));
println!("set: {}", total(BTreeSet::from([7, 7, 8])));
println!("chain: {}", total((1..=3).map(|n| n * n)));
// `for x in v` is `IntoIterator::into_iter(v)` plus a loop over next().
// Written out by hand:
let v = vec!["a", "b"];
let mut it = IntoIterator::into_iter(v);
while let Some(x) = it.next() {
println!("by hand: {x}");
}
// &Vec and &mut Vec implement IntoIterator too. That is why
// `for x in &v` borrows and `for x in &mut v` lets you change items.
let mut w = vec![1, 2];
for x in &mut w {
*x *= 100;
}
for x in &w {
println!("after &mut loop: {x}");
}
}
$ cargo run
Vec: 6
array: 9
range: 55
set: 15
chain: 14
by hand: a
by hand: b
after &mut loop: 100
after &mut loop: 200

Vec, arrays, sets and maps implement it, and so do &Vec and &mut Vec, which is why for x in &v borrows and for x in &mut v changes items. Every iterator implements it too (it turns into itself), so you can for over a chain.

A function that takes impl IntoIterator<Item = T> accepts a Vec, array, range, set or chain, as total shows. extend and zip do the same, which is why zip took an array in the adaptors table. The generics lesson explains impl Trait.

Things that waste work

  • Collecting just to iterate again. Keep it one chain; collect to keep, sort or reuse results.
  • count() when you know the length. count walks every item; a Vec and any ExactSizeIterator have len().

Practice

Try it

Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run until it prints all checks passed.

Most common words

Return the n most common words in a text with their counts, most common first. Words are runs of letters, compared in lowercase, and ties are sorted alphabetically. Watch for empty “words” between punctuation, and for words that aren’t English.

Starterexercises/word-frequency.rs
/// The `n` most common words in `text`, most common first, as (word, count).
/// A word is a run of letters (`char::is_alphabetic`), compared in lowercase.
/// Words with the same count are sorted alphabetically.
fn top_words(text: &str, n: usize) -> Vec<(String, usize)> {
todo!("count with a HashMap and entry(), then collect, sort and take(n)")
}
fn main() {
// Ties are broken alphabetically: "and" comes before "cat" and "hat".
assert_eq!(
top_words("the cat and the hat", 2),
vec![("the".to_string(), 2), ("and".to_string(), 1)]
);
// Case and punctuation do not make a different word.
assert_eq!(
top_words("Go, go! GO? rust.", 1),
vec![("go".to_string(), 3)]
);
// Letters outside English are letters too.
assert_eq!(
top_words("Ölçek ölçek straße", 5),
vec![("ölçek".to_string(), 2), ("straße".to_string(), 1)]
);
// No words at all, not one empty word.
assert_eq!(top_words("", 3), vec![]);
assert_eq!(top_words(" ... !!! ", 3), vec![]);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/word-frequency.solution.rsRun ↗
use std::collections::HashMap;
/// The `n` most common words in `text`, most common first, as (word, count).
/// A word is a run of letters (`char::is_alphabetic`), compared in lowercase.
/// Words with the same count are sorted alphabetically.
fn top_words(text: &str, n: usize) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
for word in text
.split(|c: char| !c.is_alphabetic())
.filter(|w| !w.is_empty())
{
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
let mut ranked: Vec<(String, usize)> = counts.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
ranked.into_iter().take(n).collect()
}
fn main() {
// Ties are broken alphabetically: "and" comes before "cat" and "hat".
assert_eq!(
top_words("the cat and the hat", 2),
vec![("the".to_string(), 2), ("and".to_string(), 1)]
);
// Case and punctuation do not make a different word.
assert_eq!(
top_words("Go, go! GO? rust.", 1),
vec![("go".to_string(), 3)]
);
// Letters outside English are letters too.
assert_eq!(
top_words("Ölçek ölçek straße", 5),
vec![("ölçek".to_string(), 2), ("straße".to_string(), 1)]
);
// No words at all, not one empty word.
assert_eq!(top_words("", 3), vec![]);
assert_eq!(top_words(" ... !!! ", 3), vec![]);
println!("all checks passed");
}
$ cargo run
all checks passed

Parse a list, or say what's wrong

Turn "3, 4,5" into Ok(vec![3, 4, 5]), or an Err naming the first piece that isn’t a number. Collecting into a Result does most of the work. An empty input is an empty list, which a plain split gets wrong.

Starterexercises/parse-list.rs
/// Parse "3, 4,5" into [3, 4, 5]. Spaces around each number are ignored.
/// If any piece is not a whole number, return Err("bad number: \"<piece>\"")
/// for the first such piece, with the piece trimmed.
fn parse_list(text: &str) -> Result<Vec<i32>, String> {
todo!("split on ',', trim, parse, map_err, then collect into Result<Vec<_>, _>")
}
fn main() {
assert_eq!(parse_list("3, 4,5"), Ok(vec![3, 4, 5]));
assert_eq!(parse_list(" -4 , +5 "), Ok(vec![-4, 5]));
// The first bad piece is the one reported.
assert_eq!(parse_list("1, x, y"), Err("bad number: \"x\"".to_string()));
assert_eq!(parse_list("1, 2.5"), Err("bad number: \"2.5\"".to_string()));
// Empty input is an empty list, not an error.
assert_eq!(parse_list(""), Ok(vec![]));
assert_eq!(parse_list(" "), Ok(vec![]));
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/parse-list.solution.rsRun ↗
/// Parse "3, 4,5" into [3, 4, 5]. Spaces around each number are ignored.
/// If any piece is not a whole number, return Err("bad number: \"<piece>\"")
/// for the first such piece, with the piece trimmed.
fn parse_list(text: &str) -> Result<Vec<i32>, String> {
if text.trim().is_empty() {
return Ok(Vec::new());
}
text.split(',')
.map(str::trim)
.map(|piece| {
piece
.parse::<i32>()
.map_err(|_| format!("bad number: {piece:?}"))
})
.collect()
}
fn main() {
assert_eq!(parse_list("3, 4,5"), Ok(vec![3, 4, 5]));
assert_eq!(parse_list(" -4 , +5 "), Ok(vec![-4, 5]));
// The first bad piece is the one reported.
assert_eq!(parse_list("1, x, y"), Err("bad number: \"x\"".to_string()));
assert_eq!(parse_list("1, 2.5"), Err("bad number: \"2.5\"".to_string()));
// Empty input is an empty list, not an error.
assert_eq!(parse_list(""), Ok(vec![]));
assert_eq!(parse_list(" "), Ok(vec![]));
println!("all checks passed");
}
$ cargo run
all checks passed

Longest rising stretch

Given a list of readings, find the length of the longest stretch where each reading is higher than the one before. A slice method from the grouping section makes this a one-liner. It must return 0 for no readings, and treat equal readings as not rising.

Starterexercises/longest-rise.rs
/// The length of the longest stretch where every reading is higher than the one
/// before it. A single reading on its own is a stretch of 1.
fn longest_rise(readings: &[i32]) -> usize {
todo!("chunk_by(|a, b| a < b), then the longest chunk")
}
fn main() {
assert_eq!(longest_rise(&[1, 2, 3, 1, 2]), 3);
assert_eq!(longest_rise(&[3, 1, 2, 3, 4, 0]), 4);
assert_eq!(longest_rise(&[5, 4, 3]), 1);
// Equal is not rising.
assert_eq!(longest_rise(&[2, 2, 2]), 1);
assert_eq!(longest_rise(&[7]), 1);
// No readings, no run.
assert_eq!(longest_rise(&[]), 0);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/longest-rise.solution.rsRun ↗
/// The length of the longest stretch where every reading is higher than the one
/// before it. A single reading on its own is a stretch of 1.
fn longest_rise(readings: &[i32]) -> usize {
readings
.chunk_by(|a, b| a < b)
.map(|run| run.len())
.max()
.unwrap_or(0)
}
fn main() {
assert_eq!(longest_rise(&[1, 2, 3, 1, 2]), 3);
assert_eq!(longest_rise(&[3, 1, 2, 3, 4, 0]), 4);
assert_eq!(longest_rise(&[5, 4, 3]), 1);
// Equal is not rising.
assert_eq!(longest_rise(&[2, 2, 2]), 1);
assert_eq!(longest_rise(&[7]), 1);
// No readings, no run.
assert_eq!(longest_rise(&[]), 0);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more