289: Extending Collections with extend()
Tutorial Video
Text description (accessibility)
This video demonstrates the "289: Extending Collections with extend()" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Building up a collection incrementally from multiple sources — appending new items to an existing `Vec`, merging two `HashMap`s, adding elements from a computation to an existing set — is a fundamental operation in accumulative algorithms. Key difference from OCaml: 1. **Mutability**: Rust's `extend()` mutates the collection in place, consuming the iterator; OCaml's `@` creates a new list.
Tutorial
The Problem
Building up a collection incrementally from multiple sources — appending new items to an existing Vec, merging two HashMaps, adding elements from a computation to an existing set — is a fundamental operation in accumulative algorithms. The extend() method is the mutable counterpart to collect(): it appends elements from any IntoIterator to an existing collection in place, avoiding the need to create intermediate temporary collections.
🎯 Learning Outcomes
extend() as the in-place append operation for any Extend<T> collectionextend() to merge multiple sources into a single pre-existing collectionextend on a Vec is equivalent to append but accepts any iteratorextend() with filtered or transformed iterators for selective mergingCode Example
let mut base = vec![1, 2, 3];
base.extend([4, 5, 6]); // in-place, reuses allocationKey Differences
extend() mutates the collection in place, consuming the iterator; OCaml's @ creates a new list.extend silently overwrites on key collision; explicit merge logic is needed if different behavior is desired.extend() on a Vec reserves space using size_hint() to minimize reallocations; OCaml's @ always allocates.Vec with with_capacity() then uses extend() in a loop — one allocation, multiple appends.OCaml Approach
OCaml's List.rev_append and @ operator combine lists, but these create new lists rather than mutating in place. For mutable structures, Buffer.add_string (for strings) and Hashtbl.add_seq provide equivalent in-place extension:
(* Functional: create new list combining both *)
let combined = base @ [4; 5; 6]
(* Mutable: Hashtbl extend from sequence *)
Hashtbl.add_seq tbl (List.to_seq [("b", 2); ("c", 3)])
Full Source
#![allow(clippy::all)]
//! # Extending Collections with extend()
//!
//! `extend()` appends elements from an iterator to an existing collection in place.
use std::collections::{HashMap, HashSet};
/// Extend a vector with elements from another source
pub fn extend_vec_demo() -> Vec<i32> {
let mut base = vec![1, 2, 3];
base.extend([4, 5, 6]);
base.extend(7..=9);
base
}
/// Extend a string with characters
pub fn extend_string(base: &str, suffix: &str) -> String {
let mut s = String::from(base);
s.extend(suffix.chars());
s
}
/// Extend a HashMap with new entries
pub fn extend_hashmap<'a>(
base: &mut HashMap<&'a str, i32>,
entries: impl IntoIterator<Item = (&'a str, i32)>,
) {
base.extend(entries);
}
/// Extend a HashSet (duplicates are ignored)
pub fn extend_hashset(
mut set: HashSet<i32>,
extras: impl IntoIterator<Item = i32>,
) -> HashSet<i32> {
set.extend(extras);
set
}
/// Extend with filtered iterator
pub fn extend_with_filter(
base: &mut Vec<i32>,
source: impl Iterator<Item = i32>,
predicate: impl Fn(&i32) -> bool,
) {
base.extend(source.filter(predicate));
}
/// Incremental building pattern
pub fn build_incrementally(batches: &[&[i32]]) -> Vec<i32> {
let mut result = Vec::new();
for batch in batches {
result.extend(*batch);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extend_vec_basic() {
let mut v = vec![1i32, 2, 3];
v.extend([4, 5, 6]);
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_extend_vec_demo() {
let v = extend_vec_demo();
assert_eq!(v, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_extend_string() {
let s = extend_string("hello", " world");
assert_eq!(s, "hello world");
}
#[test]
fn test_extend_with_range() {
let mut v: Vec<i32> = vec![];
v.extend(1..=5);
assert_eq!(v, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_extend_hashset_dedup() {
let set: HashSet<i32> = [1, 2].iter().copied().collect();
let extended = extend_hashset(set, [2, 3]);
assert_eq!(extended.len(), 3);
}
#[test]
fn test_extend_hashmap() {
let mut map: HashMap<&str, i32> = HashMap::new();
map.insert("a", 1);
extend_hashmap(&mut map, [("b", 2), ("c", 3)]);
assert_eq!(map.len(), 3);
assert_eq!(map["b"], 2);
}
#[test]
fn test_extend_with_filter() {
let mut evens = vec![2, 4];
extend_with_filter(&mut evens, 1..=10, |x| x % 2 == 0);
assert_eq!(evens, vec![2, 4, 2, 4, 6, 8, 10]);
}
#[test]
fn test_build_incrementally() {
let result = build_incrementally(&[&[1, 2], &[3, 4], &[5, 6]]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extend_vec_basic() {
let mut v = vec![1i32, 2, 3];
v.extend([4, 5, 6]);
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_extend_vec_demo() {
let v = extend_vec_demo();
assert_eq!(v, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_extend_string() {
let s = extend_string("hello", " world");
assert_eq!(s, "hello world");
}
#[test]
fn test_extend_with_range() {
let mut v: Vec<i32> = vec![];
v.extend(1..=5);
assert_eq!(v, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_extend_hashset_dedup() {
let set: HashSet<i32> = [1, 2].iter().copied().collect();
let extended = extend_hashset(set, [2, 3]);
assert_eq!(extended.len(), 3);
}
#[test]
fn test_extend_hashmap() {
let mut map: HashMap<&str, i32> = HashMap::new();
map.insert("a", 1);
extend_hashmap(&mut map, [("b", 2), ("c", 3)]);
assert_eq!(map.len(), 3);
assert_eq!(map["b"], 2);
}
#[test]
fn test_extend_with_filter() {
let mut evens = vec![2, 4];
extend_with_filter(&mut evens, 1..=10, |x| x % 2 == 0);
assert_eq!(evens, vec![2, 4, 2, 4, 6, 8, 10]);
}
#[test]
fn test_build_incrementally() {
let result = build_incrementally(&[&[1, 2], &[3, 4], &[5, 6]]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
}
Deep Comparison
OCaml vs Rust: extend()
Pattern 1: Append to Collection
OCaml
let base = [1; 2; 3] in
let extension = [4; 5; 6] in
let combined = base @ extension (* creates new list *)
Rust
let mut base = vec![1, 2, 3];
base.extend([4, 5, 6]); // in-place, reuses allocation
Pattern 2: String Building
OCaml
let buf = Buffer.create 16 in
Buffer.add_string buf "hello";
Buffer.add_string buf " world";
Buffer.contents buf
Rust
let mut s = String::from("Hello");
s.extend(", world!".chars());
// or: s.push_str(", world!");
Pattern 3: Incremental Building
Rust
let mut result = Vec::new();
for batch in batches {
result.extend(batch); // efficient - reuses allocation
}
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| List append | @ creates new list | extend modifies in-place |
| Allocation | Always new | Reuses when possible |
| String | Buffer.add_string | extend(chars) or push_str |
| Maps | fold_left + insert | map.extend(pairs) |
| vs chain+collect | Equivalent result | extend avoids new allocation |
Exercises
extend()-ing a HashMap<String, usize> from multiple document iterators, combining counts correctly.extend() to merge sorted chunks back into a single sorted Vec (k-way merge via repeated extend + sort, demonstrating the efficiency concern).StringBuilder wrapper around String that provides an append(iter) method using extend(), chaining multiple sources.