Hedronite Lesson · Polyglot-Dev / Rust · Tue 2026-09-08

Common Collections

Arrays fix a length at compile time. Collections grow on the heap, and each kind charges a different price for that growth.

Lesson Class: Duha (Rust language track)
Focus: Vec · String · HashMap · index vs get · UTF-8 · entry
Code Blocks: clean blocks, explanation in prose
Done-criteria: choose Vec vs String vs HashMap and use an entry
Grounding: TRPL stable Ch.8 HTML ch08-00 through ch08-03 · Blandy not cited
The vector
Contiguous heap list. Index panics; get returns Option.
The string
Owned UTF-8. Integer index refused; bytes are not letters.
The map
Keyed heap store. entry().or_insert updates without a race against the borrow checker.
Arrays fix a length at compile time. Collections grow on the heap, and each kind charges a different price for that growth.

<!-- hal:authoritative:yaml -->

Arrays fix a length at compile time. Collections grow on the heap, and each kind charges a different price for that growth.

§I — Frame

Duha session 09. TRPL Chapter 8 is one syllabus row, so this fire takes the whole chapter at chapter pace. Session 08 established packages, crates, and modules. Keep a public path in a library crate when you publish a collection helper; do not reopen Chapter 7.

So far most values lived as single owners or as fixed arrays and tuples. Growth needs three standard-library types the book names together:

  1. **Vec<T>** — a growable list of values of one type, stored contiguously.
  2. **String** — a growable, owned, UTF-8 text buffer (a specialized byte vector with text rules).
  3. **HashMap<K, V>** — a map from keys to values, also heap-backed.

By the end, choose among those three for a concrete job, read a Vec with both indexing and get, refuse integer indexing on a String for the UTF-8 reason the book gives, and update a HashMap through entry. That is the syllabus test.

§II — Why collections

Most scalar types hold one value. Collections hold many. Unlike arrays and tuples, the data a collection points to lives on the heap, so length need not be known at compile time and can change at runtime. Each collection trades capabilities for cost. Chapter 8 drills the three you meet every day; the rest of the standard library catalog sits in the docs when you need it.

§III — Vectors

Vec<T> stores more than one value of the same type next to each other in memory. File lines, cart prices, and event queues are the usual shape.

Create an empty vector with a type annotation when nothing has been pushed yet:

let v: Vec<i32> = Vec::new();

More often seed values with the vec! macro so Rust infers the element type:

let v = vec![1, 2, 3];

Grow a mutable vector with push:

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);

Read: index versus get

Two reads, two failure modes:

let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);

&v[index] panics if the index is past the end. Use it when a bad index means the program is wrong. v.get(index) returns Option<&T>: Some on hit, None on miss. Use it when the index may come from a human or another fallible source and you want to recover.

Borrowing still rules growth

Hold an immutable reference into a vector, then push, and the borrow checker refuses the push. A reallocation may move the buffer; the old reference would dangle. Chapter 4 ownership is not optional color here. It is why that compile error exists.

Iterate and mix types

Walk elements with for i in &v or mutate with for i in &mut v:

let v = vec![100, 32, 57];
for i in &v {
    println!("{i}");
}

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Vectors store one type. For a list that must hold several kinds of value, wrap them in an enum and store Vec<ThatEnum>:

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Match on the variants the way Chapter 6 taught. Dropping the vector drops its elements unless you moved them out earlier.

§IV — Strings

Rust’s core language string type is the slice str, usually seen as &str. String is the standard-library owned, growable, UTF-8 buffer. Both are UTF-8. People say “string” for either; this section is mostly about String.

Create empty, from a slice, or with String::from:

let mut s = String::new();
let s = "initial contents".to_string();
let s = String::from("initial contents");

UTF-8 means greetings in many scripts are valid String values. len counts bytes, not grapheme clusters.

Update

push_str appends a string slice. push appends one char:

let mut s = String::from("foo");
s.push_str("bar");
s.push('!');

The + operator takes ownership of the left String and borrows the right &str, so s1 + &s2 moves s1. Prefer format! when you want to keep the inputs and build a new owned string:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

Why s[0] does not compile

Many languages index a string by integer. Rust refuses:

let s1 = String::from("hi");
// let h = s1[0]; // error: `str` cannot be indexed by `{integer}`

String is a wrapper over Vec<u8>. In UTF-8, one displayed character may be one byte (Hola → 4) or several (Здравствуйте → 24 bytes for twelve scalar values). Index 0 might land on a byte that is not a complete character. Returning that bare byte would surprise you; panicking later would be worse. The compiler rejects the index so the mistake cannot ship.

Three views of the same text:

  1. Bytesu8 values.
  2. Scalar values — Unicode scalar values (char).
  3. Grapheme clusters — what a reader calls a letter (needs a crate for iteration).

Iterate with .bytes(), .chars(), or a grapheme crate when that is the unit you mean. Slice with byte ranges only when you know the range sits on character boundaries; a bad cut panics.

§V — Hash maps

HashMap<K, V> associates keys with values. It is not in the prelude:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

get returns Option<&V>. A common pattern copies out an owned value with a default:

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

Iterate with for (key, value) in &scores. Order is arbitrary.

Ownership on insert

Types that implement Copy (like i32) copy into the map. Owned values like String move. After insert(field_name, field_value), those bindings are gone. Inserting references is possible only while the referents outlive the map; lifetimes in Chapter 10 make that precise.

Overwrite, or insert only if absent

A second insert on the same key replaces the value. To insert only when missing, use entry:

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

or_insert returns &mut V to the existing or newly inserted value. That is the clean API the borrow checker prefers over hand-rolled check-then-insert.

Update from the old value

Word counts are the book’s canonical loop:

let text = "hello world wonderful world";
let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

entry yields a mutable reference; dereference to increment. The reference ends with the loop body, so the next iteration may borrow again.

Default hashing is SipHash: slower than some alternatives, harder to turn into a denial-of-service on attacker-controlled keys. Swap the hasher later when profiling proves you need it (Chapter 10 traits).

§VI — One complete proof

Pick the structure for each job:

JobReach for
Ordered list of one type, length unknownVec<T>
Owned editable textString
Lookup by keyHashMap<K, V>

Then exercise the sharp edges:

  1. Build a Vec<i32>, read with &v[i] where panic is correct, and with v.get(i) where None is correct.
  2. Hold &v[0] and try v.push(...) — watch the compiler stop the push.
  3. Build a String, append with push_str / format!, and confirm s[0] does not compile.
  4. Fill a HashMap with entry(...).or_insert(...) and bump a count through the mutable reference.

When those four hold, Chapter 8’s selected depth is done.

§VII — Book exercises (optional drill)

The chapter closes with three drills worth a quiet hour after this fire: median and mode of a Vec<i32> (sort plus a HashMap for mode), Pig Latin on UTF-8 words, and a department roster that maps department names to vectors of employee names. They are practice, not this lesson's done-criteria.

§VIII — Closing

Collections are heap growth with a contract. Vectors give contiguous lists and a panic-versus-Option choice on read. Strings give UTF-8 text and refuse dishonest integer indexes. Hash maps give keyed storage and an entry path that updates without fighting the borrow checker. Error handling is next in the book; keep these three types in hand when operations start returning Result.

Done-criteria: choose Vec versus String versus HashMap, and use an entry.

Related