# Collections, closures & iterators

> Use Vec, HashMap and HashSet, write closures that capture by reference or move, and build iterator chains that compile to tight loops.

Rust roadmap · Core language · 18 min read · https://levelcode.org/roadmaps/rust/collections-and-iterators

Most real code is about collections: load some records, filter them, group them, sum them. Rust's standard collections are few and well designed, and its iterators let you express those transformations as a chain of small steps that compiles down to the same machine code as a hand-written loop. Closures tie it together. This lesson covers `Vec`, `HashMap` and `HashSet`, how closures capture their environment, and the iterator adapters you'll use every day.

## Vec

`Vec<T>` is a growable array on the heap, and the collection you'll reach for 90% of the time.

```rust
fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(3);
    v.push(1);
    v.extend([4, 1, 5]);

    let w = vec![9, 2, 6]; // the vec! macro builds one from a list

    println!("{} {}", v.len(), v[0]);       // 5 3
    println!("{:?}", v.get(10));            // None instead of a panic
    println!("{:?}", v.last());             // Some(5)

    v.sort();                               // in place: [1, 1, 3, 4, 5]
    v.dedup();                              // remove consecutive duplicates: [1, 3, 4, 5]
    v.retain(|&x| x != 4);                  // keep matching: [1, 3, 5]
    let popped = v.pop();                   // Some(5)
    println!("{v:?} {popped:?} {}", w.contains(&2));
}
```

A `Vec` has a length and a capacity. When a push would exceed the capacity, it allocates a bigger buffer (roughly doubling) and moves the elements. If you know the size up front, `Vec::with_capacity(n)` avoids the regrowth.

Three ways to loop, and what each one does to the vector:

```rust
fn main() {
    let mut names = vec![String::from("ana"), String::from("bo")];

    for n in &names { println!("{n}"); }          // &String: borrow each element
    for n in &mut names { n.push('!'); }          // &mut String: modify in place
    for n in names { println!("owned {n}"); }     // String: consumes the Vec
    // names can't be used here: the last loop moved it
}
```

`for x in &v` is shorthand for `v.iter()`, `&mut v` for `v.iter_mut()`, and plain `v` for `v.into_iter()`.

## HashMap and HashSet

`HashMap<K, V>` maps keys to values. Keys must implement `Eq` and `Hash`; strings, integers, tuples of those, and your own types with `#[derive(PartialEq, Eq, Hash)]` all do.

```rust
use std::collections::HashMap;

fn main() {
    let mut stock: HashMap<String, u32> = HashMap::new();
    stock.insert("apple".to_string(), 3);
    stock.insert("pear".to_string(), 0);

    match stock.get("apple") {          // look up by &str: no allocation needed
        Some(n) => println!("{n} apples"),
        None => println!("no apples"),
    }

    if let Some(n) = stock.get_mut("pear") {
        *n += 10;
    }

    stock.remove("apple");
    for (fruit, count) in &stock {      // iteration order is unspecified
        println!("{fruit}: {count}");
    }
}
```

The **entry API** is the idiomatic way to insert-or-update in one lookup:

```rust
use std::collections::HashMap;

fn main() {
    let text = "the cat and the hat and the bat";
    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    println!("{}", counts["the"]); // 3

    let mut groups: HashMap<usize, Vec<&str>> = HashMap::new();
    for word in text.split_whitespace() {
        groups.entry(word.len()).or_default().push(word);
    }
    println!("{:?}", groups.get(&3).map(|g| g.len())); // Some(8)
}
```

`entry(key)` returns a handle to the slot, and `or_insert` / `or_default` / `or_insert_with` fill it if it's empty, then give you a `&mut V`.

`HashSet<T>` stores unique values and supports set operations (`union`, `intersection`, `difference`). Need sorted keys? Use `BTreeMap` and `BTreeSet`, which keep keys ordered and iterate in order. `VecDeque` is a double-ended queue for when you push and pop at both ends.

> [!NOTE]
> The default hasher (SipHash) is chosen to resist HashDoS attacks, where an attacker sends keys crafted to collide. It's slower than simpler hashes. For maps keyed by trusted data in hot paths, crates like `rustc-hash` or `ahash` are faster; keep the default for anything keyed by user input.

## Closures

A **closure** is an anonymous function that can capture variables from the scope it's defined in:

```rust
fn main() {
    let threshold = 10;
    let is_big = |n: i32| n > threshold; // captures threshold by reference
    println!("{} {}", is_big(5), is_big(50));

    let add = |a, b| a + b;              // parameter and return types inferred
    println!("{}", add(2, 3));
}
```

The compiler picks the least powerful capture that works:

- **By shared reference** if the closure only reads the variable.
- **By mutable reference** if it modifies it.
- **By value (move)** if it consumes it, for example by returning or dropping it.

```rust
fn main() {
    let mut log = Vec::new();
    let mut record = |msg: &str| log.push(msg.to_string()); // borrows log mutably
    record("start");
    record("stop");
    println!("{log:?}"); // fine: the closure's borrow ended at its last use
}
```

`move` forces every captured variable to be moved (or copied) into the closure. You need it when the closure must outlive the current scope, as with threads and async tasks:

```rust
use std::thread;

fn main() {
    let name = String::from("worker-1");
    let handle = thread::spawn(move || {
        println!("hello from {name}"); // name was moved into the closure
    });
    handle.join().unwrap();
}
```

Functions that accept closures say what they'll do with them through three traits:

| Trait | The closure can be called… | Example consumer |
| --- | --- | --- |
| `FnOnce` | at least once; may consume captured values | `Option::unwrap_or_else`, `thread::spawn` |
| `FnMut` | many times; may mutate captured values | `Iterator::for_each`, `Vec::retain` |
| `Fn` | many times, even concurrently; only reads captures | Callbacks shared between threads |

Every `Fn` is also `FnMut`, and every `FnMut` is also `FnOnce`. When you write a function that takes a closure, ask for the most permissive trait that works for you (prefer `FnOnce` over `FnMut` over `Fn`), so callers can pass the widest range of closures.

```rust
fn retry<T, E>(times: u32, mut op: impl FnMut() -> Result<T, E>) -> Result<T, E> {
    let mut last = op();
    for _ in 1..times {
        if last.is_ok() { break; }
        last = op();
    }
    last
}
```

## Iterators

An iterator is anything that implements the `Iterator` trait, which has one required method:

```rust
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
```

Each call to `next` yields `Some(item)` until the iterator is exhausted, then `None`. A `for` loop is just repeated calls to `next`.

On top of `next`, the trait provides dozens of methods. **Adapters** take an iterator and return a new one; they're **lazy** and do nothing until something consumes them. **Consumers** drive the iteration and produce a result.

```rust
fn main() {
    let orders = vec![("ana", 120), ("bo", 40), ("cy", 300), ("ana", 80)];

    let big_total: i32 = orders
        .iter()
        .filter(|(_, amount)| *amount >= 100)   // adapter
        .map(|(_, amount)| amount)              // adapter
        .sum();                                 // consumer
    println!("{big_total}"); // 420

    let names: Vec<&str> = orders.iter().map(|(name, _)| *name).collect();
    println!("{names:?}");
}
```

The adapters you'll use most:

| Adapter | Does |
| --- | --- |
| `map(f)` | Transforms each item |
| `filter(p)` | Keeps items where `p` is true |
| `filter_map(f)` | Maps to `Option` and drops the `None`s — map and filter in one |
| `flat_map(f)` / `flatten()` | Maps to an iterator and concatenates the results |
| `enumerate()` | Pairs each item with its index |
| `zip(other)` | Pairs items from two iterators |
| `take(n)` / `skip(n)` | The first `n` / everything after the first `n` |
| `take_while(p)` / `skip_while(p)` | Stop or start at the first item failing / passing `p` |
| `chain(other)` | One iterator after another |
| `rev()` | Reverse (for iterators that can run backwards) |
| `peekable()` | Lets you look at the next item without consuming it |

And consumers:

| Consumer | Returns |
| --- | --- |
| `collect()` | A collection: `Vec`, `HashMap`, `String`, `HashSet`… |
| `sum()` / `product()` / `count()` | A number |
| `min()` / `max()` / `min_by_key(f)` / `max_by_key(f)` | `Option<Item>` |
| `find(p)` / `position(p)` | The first match / its index, as `Option` |
| `any(p)` / `all(p)` | `bool`, stopping early |
| `fold(init, f)` | A single accumulated value |
| `for_each(f)` | Nothing; runs `f` for its side effects |

`collect` is driven by the type you ask for. Turbofish syntax (`collect::<Vec<_>>()`) or a type annotation on the binding tells it what to build. It can even collect an iterator of `Result`s into a `Result` of a collection, stopping at the first error:

```rust
fn main() {
    let inputs = ["1", "2", "x", "4"];
    let parsed: Result<Vec<i32>, _> = inputs.iter().map(|s| s.parse::<i32>()).collect();
    println!("{parsed:?}"); // Err(ParseIntError { kind: InvalidDigit })

    let valid: Vec<i32> = inputs.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{valid:?}"); // [1, 2, 4]

    let joined = valid.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", ");
    println!("{joined}"); // 1, 2, 4
}
```

### Laziness and zero cost

Because adapters are lazy, nothing happens until a consumer runs, and then each item flows through the entire chain before the next one starts. No intermediate vectors are built. `(1..).map(|n| n * n).filter(|n| n % 3 == 1).take(5)` works on an infinite range because `take(5)` stops pulling after five items.

The compiler inlines the whole chain into one loop. Iterator code is usually exactly as fast as the indexed loop you'd write by hand, and sometimes faster, because it can skip bounds checks.

> [!WARNING]
> An iterator chain without a consumer does nothing. `v.iter().map(|x| println!("{x}"));` prints nothing, and the compiler warns that the iterator is unused. Use a `for` loop or `for_each` for side effects.

### iter, iter_mut and into_iter

`iter()` yields `&T`, `iter_mut()` yields `&mut T`, and `into_iter()` yields `T` by value (consuming the collection). That's why closures in chains over `.iter()` see references, and why you'll write `|&x|` or `*x` to get at a `Copy` value. When the types confuse you, hover over the closure parameter in your editor.

## Practice

You have web server log lines in this format: `"GET /api/users 200 35"` (method, path, status, milliseconds).

1. Write `fn parse(line: &str) -> Option<Request>` using `split_whitespace` and `?` on `Option`. `Request` has `method: String`, `path: String`, `status: u16`, `ms: u32`.
2. From a `Vec<&str>` of lines, using iterator chains only (no explicit loops), compute:
   - the number of lines that failed to parse,
   - the average latency of successful (2xx) requests,
   - a `HashMap<String, usize>` of request counts per path,
   - the slowest request's path.

**Solution**

```rust
use std::collections::HashMap;

#[derive(Debug)]
struct Request {
    method: String,
    path: String,
    status: u16,
    ms: u32,
}

fn parse(line: &str) -> Option<Request> {
    let mut parts = line.split_whitespace();
    Some(Request {
        method: parts.next()?.to_string(),
        path: parts.next()?.to_string(),
        status: parts.next()?.parse().ok()?,
        ms: parts.next()?.parse().ok()?,
    })
}

fn main() {
    let lines = vec![
        "GET /api/users 200 35",
        "POST /api/users 201 80",
        "GET /api/users 500 1200",
        "garbage",
        "GET /health 200 2",
    ];

    let requests: Vec<Request> = lines.iter().filter_map(|l| parse(l)).collect();
    let failed = lines.len() - requests.len();

    let ok: Vec<u32> = requests
        .iter()
        .filter(|r| (200..300).contains(&r.status))
        .map(|r| r.ms)
        .collect();
    let avg = ok.iter().sum::<u32>() as f64 / ok.len().max(1) as f64;

    let per_path = requests.iter().fold(HashMap::new(), |mut acc, r| {
        *acc.entry(r.path.clone()).or_insert(0usize) += 1;
        acc
    });

    let slowest = requests.iter().max_by_key(|r| r.ms).map(|r| r.path.as_str());

    println!("failed: {failed}");               // 1
    println!("avg 2xx ms: {avg:.1}");           // 39.0
    println!("per path: {per_path:?}");
    println!("slowest: {slowest:?}");           // Some("/api/users")
    println!("first method: {}", requests[0].method);
}
```

`parse` uses `?` on `Option`: any missing field or failed number returns `None` from the whole function. `.ok()?` turns a `Result` into an `Option` first. `ok.len().max(1)` avoids dividing by zero when there are no successful requests. `fold` builds the map in one pass; a `for` loop with `entry` is equally idiomatic and often clearer.

## Key takeaways

- `Vec<T>` is the default collection; `HashMap`/`HashSet` for lookup by key, `BTreeMap` when you need ordered keys.
- The entry API (`entry(k).or_insert(…)`) inserts or updates in a single lookup.
- Closures capture by reference, mutable reference or value; `move` forces ownership into the closure, which threads and tasks need.
- `FnOnce`, `FnMut` and `Fn` describe how a closure may be called; accept the most permissive one that works.
- Iterator adapters are lazy and compile to tight loops; a consumer such as `collect`, `sum` or `find` drives them.

## Quiz

1. What does this print? let v = vec![1, 2, 3]; v.iter().map(|x| { println!("{x}"); x * 2 });

   A. 1 2 3
   B. 2 4 6
   C. Nothing: map is lazy and no consumer runs it
   D. It doesn't compile

   **Answer: C.** Adapters like map do nothing until consumed. The compiler warns about an unused iterator. Use a for loop or for_each for side effects.

2. Which is the idiomatic way to count word frequencies in a HashMap<&str, usize>?

   A. *counts.entry(word).or_insert(0) += 1;
   B. counts.insert(word, counts[word] + 1);
   C. if counts.contains_key(word) { counts.remove(word); } counts.insert(word, 1);
   D. counts.get(word).unwrap() + 1;

   **Answer: A.** entry finds or creates the slot in one lookup and or_insert returns a &mut usize to increment. Indexing with counts[word] panics if the key is missing.

3. Why do you usually need move when passing a closure to thread::spawn?

   A. Threads can only run FnMut closures
   B. The thread may outlive the current function, so it can't hold references to its locals
   C. move makes the closure run faster
   D. Without move, the closure runs on the current thread

   **Answer: B.** thread::spawn requires a 'static closure because the new thread may keep running after the spawning function returns. move transfers ownership of captured values into the closure.

4. What does ["1", "x", "3"].iter().map(|s| s.parse::<i32>()).collect::<Result<Vec<_>, _>>() produce?

   A. Ok([1, 3])
   B. Ok([1, 0, 3])
   C. A compile error: can't collect into a Result
   D. Err(…) for "x", stopping at the first failure

   **Answer: D.** Collecting an iterator of Results into Result<Vec<_>, E> yields Ok with all values if every item is Ok, or the first Err otherwise. Use filter_map with .ok() to skip failures instead.
