Rust from the ground up

Vectors, maps and iterators

The two collections you will reach for first — a growable list and a lookup by key — and the iterator chain that turns a loop into a sentence.

Part 9 of 12

Intermediate18 min readBack to Courses

Vec: a list that grows

A Vec<T> is a list you can append to: let mut scores = vec![42, 17, 88]; then scores.push(5);. Indexing with scores[0] panics if the index is past the end; scores.get(0) returns an Option instead, which is the honest form when the index comes from somewhere you do not control.

A vector keeps its items in order and next to each other in memory, which makes walking it fast. Sorting is a method — scores.sort() — and the element type must know how to be compared, which the standard numbers and text do.

Pushing may need to move the whole list into a bigger allocation, which is why the compiler will not let you hold a reference to an item while the vector grows. let first = &scores[0]; scores.push(1); is a borrow error, not a run-time surprise. Vec::with_capacity(n) avoids the repeated moves when you know how many items are coming.

HashMap: a value per key

A HashMap<K, V> looks a value up by key: let mut counts = HashMap::new(); then counts.insert("ada", 3); and counts.get("ada") — which returns an Option, because the key may not be there. entry(key).or_insert(0) is the pattern for counting: it gives you the slot, creating it with a starting value if needed.

Hash maps have no order. When you print one, the pairs come out in whatever order suits the map, which is exactly what the debug format warns you about.

Iterators turn loops into sentences

Every collection hands out iterators: .iter() borrows each item, .into_iter() takes ownership, .iter_mut() lends each mutably. From there, the chain: .map(...) changes each item, .filter(...) drops the ones that fail a test, .sum() and .count() finish with a number, .collect() gathers into a new vector or map.

The closures are small and inline: |n| n + n, |word| word.len() > 3. An iterator chain is lazy until something finishes it, and it compiles to the same loop you would have written — the chain is for the reader.

A list, a map, and a chain

use std::collections::HashMap;

fn main() {
    let mut scores = vec![42, 17, 88, 5, 63];
    scores.push(50);

    let doubled: Vec<i32> = scores.iter().map(|n| n * 2).collect();
    let total: i32 = scores.iter().sum();
    let big: Vec<&i32> = scores.iter().filter(|n| **n > 50).collect();

    println!("doubled: {:?}", doubled);
    println!("total: {}, over fifty: {:?}", total, big);

    let words = ["the", "quick", "the", "lazy", "the"];
    let mut counts: HashMap<&str, u32> = HashMap::new();
    for word in words {
        *counts.entry(word).or_insert(0) += 1;
    }
    println!("{:?}", counts);
    println!("the appears {} times", counts.get("the").unwrap_or(&0));
}

Count the words, rank the scores

Run it, then change the tally: make the map count total letters per word instead of occurrences, and print only the words longer than three characters using a filter.

Output
Click "Run" to execute your code

get returns Option<&V>, so reading a map is a case you handle, not a value you assume. unwrap_or(&0) is the common answer when a missing key should simply count as zero.

Which iterator owns the values

The method decides what the chain sees. .iter() lends each item as &T, .iter_mut() as &mut T, and .into_iter() takes the collection apart and yields T. That is why a chain over .iter() produces references, and why collecting into a Vec<i32> from one needs .copied() or .cloned() first.

Closures see the reference too. When the iterator yields &i32, filter hands its closure a &&i32, so the comparison needs two dereferences: filter(|n| **n > 50). It looks fussy for one line, but it is the ownership rule being consistent — the chain never copies what it was only lent.

Order and lookup

A HashMap trades order for speed: lookup by key is expected constant time, and iteration order is deliberately unspecified. When the output has to be stable — a report, a test, a printed list — collect the pairs into a Vec and sort it, or use a BTreeMap, which keeps keys in order at a small cost per operation.

A Vec is for a sequence you walk; a map is for a question you ask by key. Sorting a vector is sort() for numbers and sort_by or sort_by_key for anything else. dedup() removes neighbours that are equal, so sort first, or it only removes what happened to be adjacent.

When a loop is the clearer sentence

Iterator chains are not a rule. When the body needs to print, return early, or change two things at once, a for loop says it better and the compiler produces much the same code. A chain earns its place when each step transforms the whole sequence — filter, map, sum; a loop earns it when the steps depend on each other.

A useful test: if the closure would need a block with several statements inside it, write the loop instead. The chain is there to make a transformation readable, not to prove that you know the methods.

Three collection mistakes. scores[10] on a six-item vector panics, so use get when the index comes from outside. Reading a missing key with counts["missing"] panics too — a map is read with get, which returns an Option. And HashMap iteration order is not stable between runs, so a test that compares a printed map is a test that fails eventually.

Try these next

  • Count how often each character appears in a sentence with a HashMap<char, u32>, then print the pairs sorted by count.
  • Turn a Vec<&str> into a Vec<usize> of lengths with map, then find the longest with max.
  • Print the same HashMap twice and compare the order, then collect and sort it to make the output stable.

A leaderboard out of a map

Run it, then add two more votes and give the tied names a stable order. Change the filter to show names with exactly one vote instead of more than one.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the difference between scores[0] and scores.get(0)?