Guide contents

Lesson 39 of 94

Iterator Adapters: The Complete Cookbook

This lesson is a cookbook: one adapter or consumer per section, each with a small realistic example. Skim the table, then jump to what you need.

Rust Section
map 1
filter 2
filter_map 3
flat_map 4
flatten 5
enumerate 6
zip 7
chain 8
take, skip 9
take_while, skip_while 10
step_by 11
rev 12
fold 13
reduce 14
scan 15
sum, product, count 16
min, max, min_by_key, max_by_key, max_by 17
find, find_map 18
position, rposition 19
any, all 20
partition 21
unzip 22
inspect 23
peekable 24
cycle 25
last, nth 26
chunk_by (on slices) 27
dedup (on Vec) 28
sorted via collect + sort 29
collect to String, join 30

1. map: transform every item

Rust
fn main() {
    let celsius = [0.0, 21.5, 37.0, 100.0];
    let fahrenheit: Vec<f64> = celsius.iter().map(|c| c * 9.0 / 5.0 + 32.0).collect();
    println!("{:?}", fahrenheit);
}
Output
[32.0, 70.7, 98.6, 212.0]

2. filter: keep matching items

The closure receives a reference to each item, so with iter() over numbers you get &&i32. Patterns like |&&x| or |x| **x > ... handle that; so does comparing references.

Rust
fn main() {
    let latencies_ms = [120, 950, 85, 1_400, 300];
    let slow: Vec<u32> = latencies_ms.iter().copied().filter(|&ms| ms > 500).collect();
    let slow_refs: Vec<&u32> = latencies_ms.iter().filter(|ms| **ms > 500).collect();
    println!("{:?} {:?}", slow, slow_refs);
}
Output
[950, 1400] [950, 1400]

3. filter_map: transform and drop failures in one step

The closure returns an Option: Some(value) keeps the transformed value, None drops the item. It's the idiomatic way to parse and skip invalid entries.

Rust
fn main() {
    let raw = ["42", "  7 ", "abc", "", "100"];
    let numbers: Vec<i32> = raw.iter().filter_map(|s| s.trim().parse().ok()).collect();
    println!("{:?}", numbers);

    let emails = ["ana@example.com", "not-an-email", "ben@corp.com"];
    let domains: Vec<&str> = emails.iter().filter_map(|e| e.split_once('@').map(|(_, d)| d)).collect();
    println!("{:?}", domains);
}
Output
[42, 7, 100]
["example.com", "corp.com"]

4. flat_map: one item to many

Rust
fn main() {
    let orders = [("ord-1", vec!["book", "pen"]), ("ord-2", vec![]), ("ord-3", vec!["lamp"])];
    let all_items: Vec<&str> = orders.iter().flat_map(|(_, items)| items.iter().copied()).collect();
    println!("{:?}", all_items);

    let sentences = ["Rust is fast", "Iterators compose"];
    let words: Vec<&str> = sentences.iter().flat_map(|s| s.split(' ')).collect();
    println!("{:?}", words);
}
Output
["book", "pen", "lamp"]
["Rust", "is", "fast", "Iterators", "compose"]

5. flatten: remove one level of nesting

flatten works on an iterator of iterables: a Vec<Vec<T>>, or an iterator of Options (dropping the Nones).

Rust
fn main() {
    let batches = vec![vec![1, 2], vec![], vec![3, 4, 5]];
    let all: Vec<i32> = batches.into_iter().flatten().collect();
    println!("{:?}", all);

    let maybe_ids = [Some(7), None, Some(9), None];
    let ids: Vec<i32> = maybe_ids.iter().flatten().copied().collect();
    println!("{:?}", ids);
}
Output
[1, 2, 3, 4, 5]
[7, 9]

6. enumerate: pair each item with its index

Rust
fn main() {
    let lines = ["fn main() {", "    println!(\"hi\")", "}"];
    for (n, line) in lines.iter().enumerate() {
        println!("{:>3} | {line}", n + 1);
    }
    let first_long = lines.iter().enumerate().find(|(_, l)| l.len() > 12).map(|(i, _)| i + 1);
    println!("first long line: {:?}", first_long);
}
Output
  1 | fn main() {
  2 |     println!("hi")
  3 | }
first long line: Some(2)

7. zip: walk two iterators in step

zip pairs items from two iterators and stops at the shorter one.

Rust
fn main() {
    let names = ["cpu", "memory", "disk"];
    let values = [91.5, 62.0, 78.25];
    let limits = [90.0, 80.0, 95.0];
    for ((name, value), limit) in names.iter().zip(values).zip(limits) {
        let status = if value > limit { "ALERT" } else { "ok" };
        println!("{name:<7} {value:>6.2} / {limit:<5} {status}");
    }

    let a = [1, 2, 3];
    let b = [10, 20, 30];
    let dot: i32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    println!("dot product {dot}");
}
Output
cpu      91.50 / 90    ALERT
memory   62.00 / 80    ok
disk     78.25 / 95    ok
dot product 140

8. chain: one sequence after another

Rust
fn main() {
    let defaults = vec!["--color", "--quiet"];
    let user_args = vec!["build", "--release"];
    let all: Vec<&str> = defaults.iter().chain(user_args.iter()).copied().collect();
    println!("{:?}", all);

    let header = std::iter::once("id,name");
    let rows = ["1,ana", "2,ben"].into_iter();
    let csv: Vec<&str> = header.chain(rows).collect();
    println!("{}", csv.join("\n"));
}
Output
["--color", "--quiet", "build", "--release"]
id,name
1,ana
2,ben

std::iter::once(x) is an iterator that yields a single item.

9. take and skip: pagination

Rust
fn main() {
    let results: Vec<u32> = (1..=23).collect();
    let page_size = 10;
    for page in 0..3 {
        let items: Vec<u32> = results.iter().skip(page * page_size).take(page_size).copied().collect();
        println!("page {page}: {:?}", items);
    }
}
Output
page 0: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
page 1: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 2: [21, 22, 23]

10. take_while and skip_while

These stop or start based on a condition rather than a count. They are ideal for sorted data and headers.

Rust
fn main() {
    let sorted_timestamps = [100, 250, 400, 900, 1_300, 2_000];
    let before_1000: Vec<u32> = sorted_timestamps.iter().copied().take_while(|&t| t < 1_000).collect();
    let from_1000: Vec<u32> = sorted_timestamps.iter().copied().skip_while(|&t| t < 1_000).collect();
    println!("{:?} {:?}", before_1000, from_1000);

    let file = "# comment\n# another\nkey=value\n# not skipped\nother=1";
    let body: Vec<&str> = file.lines().skip_while(|l| l.starts_with('#')).collect();
    println!("{:?}", body);
}
Output
[100, 250, 400, 900] [1300, 2000]
["key=value", "# not skipped", "other=1"]

11. step_by: every nth item

Rust
fn main() {
    let samples: Vec<u32> = (0..20).map(|i| i * 3).collect();
    let downsampled: Vec<u32> = samples.iter().step_by(5).copied().collect();
    println!("{:?}", downsampled);
    let odd_positions: Vec<char> = "abcdefg".chars().skip(1).step_by(2).collect();
    println!("{:?}", odd_positions);
}
Output
[0, 15, 30, 45]
['b', 'd', 'f']

12. rev: iterate backwards

Most iterators over collections and ranges can run backwards, because they implement DoubleEndedIterator.

Rust
fn main() {
    let history = ["/home", "/products", "/products/42", "/cart"];
    let recent_first: Vec<&str> = history.iter().rev().copied().collect();
    println!("{:?}", recent_first);
    let last_two: Vec<&&str> = history.iter().rev().take(2).collect();
    println!("{:?}", last_two);
    let countdown: String = (1..=5).rev().map(|n| n.to_string()).collect::<Vec<_>>().join(" ");
    println!("{countdown}");
}
Output
["/cart", "/products/42", "/products", "/home"]
["/cart", "/products/42"]
5 4 3 2 1

13. fold: combine everything into one value

fold(initial, |accumulator, item| ...) is the general way to reduce a sequence. sum, count, and max are all special cases of it.

Rust
fn main() {
    let transactions = [("deposit", 500), ("withdraw", 120), ("deposit", 75), ("withdraw", 900)];
    let (balance, rejected) = transactions.iter().fold((0_i64, 0), |(bal, rej), (kind, amount)| match *kind {
        "deposit" => (bal + amount, rej),
        _ if *amount <= bal => (bal - amount, rej),
        _ => (bal, rej + 1),
    });
    println!("balance {balance}, rejected {rejected}");

    let csv_line = ["a", "b", "c"].iter().fold(String::new(), |acc, s| if acc.is_empty() { s.to_string() } else { acc + "," + s });
    println!("{csv_line}");
}
Output
balance 455, rejected 1
a,b,c

14. reduce: fold without an initial value

reduce uses the first item as the starting value, so it returns an Option (the iterator might be empty).

Rust
fn main() {
    let durations = [30, 45, 15];
    let total = durations.iter().copied().reduce(|a, b| a + b);
    let longest_word = ["iter", "reduce", "fold"].into_iter().reduce(|a, b| if b.len() > a.len() { b } else { a });
    let empty: [u32; 0] = [];
    println!("{:?} {:?} {:?}", total, longest_word, empty.iter().copied().reduce(|a, b| a + b));
}
Output
Some(90) Some("reduce") None

15. scan: running values

scan is like fold, but yields every intermediate state. It's how you compute running totals and balances.

Rust
fn main() {
    let daily_changes = [100, -30, 45, -200, 60];
    let balances: Vec<i32> = daily_changes
        .iter()
        .scan(1_000, |balance, change| {
            *balance += change;
            Some(*balance)
        })
        .collect();
    println!("{:?}", balances);

    let readings = [3.0, 5.0, 4.0, 8.0];
    let running_avg: Vec<f64> = readings
        .iter()
        .scan((0.0, 0), |(sum, n), &x| {
            *sum += x;
            *n += 1;
            Some(*sum / *n as f64)
        })
        .collect();
    println!("{:?}", running_avg);
}
Output
[1100, 1070, 1115, 915, 975]
[3.0, 4.0, 4.0, 5.0]

16. sum, product, and count

Rust
fn main() {
    let cart = [(2, 1_250), (1, 4_999), (3, 199)]; // (quantity, unit price in cents)
    let items: u32 = cart.iter().map(|(q, _)| q).sum();
    let total_cents: u32 = cart.iter().map(|(q, p)| q * p).sum();
    let lines = cart.iter().count();
    let combinations: u64 = (1..=4).product();
    println!("{lines} lines, {items} items, total {:.2}, {combinations} orderings", total_cents as f64 / 100.0);
}
Output
3 lines, 6 items, total 80.96, 24 orderings

17. min, max, and their _by_key and _by variants

max_by_key picks the item with the largest key. max_by takes a comparison closure, which is needed for floats.

Rust
fn main() {
    let employees: [(&str, u32, f64); 3] = [("ana", 34, 9_000.0), ("ben", 51, 7_200.0), ("cy", 28, 9_800.0)];
    let oldest = employees.iter().max_by_key(|(_, age, _)| *age);
    let youngest = employees.iter().min_by_key(|(_, age, _)| *age);
    let top_paid = employees.iter().max_by(|a, b| a.2.total_cmp(&b.2));
    println!("{:?}\n{:?}\n{:?}", oldest, youngest, top_paid);
    let longest_name = employees.iter().map(|(n, _, _)| n.len()).max();
    println!("{:?}", longest_name);
}
Output
Some(("ben", 51, 7200.0))
Some(("cy", 28, 9800.0))
Some(("cy", 28, 9800.0))
Some(3)

When two items tie, max_by_key returns the last one and min_by_key returns the first one.

18. find and find_map

find returns the first matching item. find_map is filter_map followed by next: it returns the first successful transformation.

Rust
fn main() {
    let servers = [("web-1", false), ("web-2", true), ("web-3", true)];
    let first_healthy = servers.iter().find(|(_, healthy)| *healthy).map(|(name, _)| *name);
    println!("{:?}", first_healthy);

    let config_lines = ["# comment", "timeout = abc", "port = 8080", "port = 9090"];
    let port: Option<u16> = config_lines
        .iter()
        .find_map(|line| line.strip_prefix("port = ").and_then(|v| v.parse().ok()));
    println!("{:?}", port);
}
Output
Some("web-2")
Some(8080)

19. position and rposition

Rust
fn main() {
    let log = ["INFO boot", "WARN low disk", "ERROR crash", "INFO restart", "ERROR crash again"];
    let first_error = log.iter().position(|l| l.starts_with("ERROR"));
    let last_error = log.iter().rposition(|l| l.starts_with("ERROR"));
    println!("{:?} {:?}", first_error, last_error);
    if let Some(i) = first_error {
        println!("context before first error: {:?}", &log[..i]);
    }
}
Output
Some(2) Some(4)
context before first error: ["INFO boot", "WARN low disk"]

20. any and all

Rust
fn main() {
    let password = "Tr0ub4dor&3";
    let checks = [
        ("length >= 10", password.len() >= 10),
        ("has digit", password.chars().any(|c| c.is_ascii_digit())),
        ("has symbol", password.chars().any(|c| !c.is_alphanumeric())),
        ("all printable", password.chars().all(|c| !c.is_control())),
    ];
    for (name, ok) in checks {
        println!("{name:<14} {ok}");
    }
    println!("strong: {}", checks.iter().all(|(_, ok)| *ok));
}
Output
length >= 10   true
has digit      true
has symbol     true
all printable  true
strong: true

21. partition: split into two collections

Rust
fn main() {
    let files = ["main.rs", "README.md", "lib.rs", "Cargo.toml", "utils.rs"];
    let (rust, other): (Vec<&str>, Vec<&str>) = files.iter().partition(|f| f.ends_with(".rs"));
    println!("rust: {:?}\nother: {:?}", rust, other);
}
Output
rust: ["main.rs", "lib.rs", "utils.rs"]
other: ["README.md", "Cargo.toml"]

22. unzip: split pairs into two collections

Rust
fn main() {
    let points = [(1.0, 2.0), (3.5, -1.0), (0.0, 4.25)];
    let (xs, ys): (Vec<f64>, Vec<f64>) = points.iter().copied().unzip();
    println!("{:?} {:?}", xs, ys);

    let (names, scores): (Vec<&str>, Vec<u32>) = [("ana", 91), ("ben", 78)].into_iter().unzip();
    println!("{:?} {:?}", names, scores);
}
Output
[1.0, 3.5, 0.0] [2.0, -1.0, 4.25]
["ana", "ben"] [91, 78]

23. inspect: look at items without changing them

inspect runs a closure on each item as it passes, which is handy for debugging a pipeline.

Rust
fn main() {
    let total: i32 = [4, -2, 7, -5, 3]
        .iter()
        .inspect(|x| println!("in:       {x}"))
        .filter(|&&x| x > 0)
        .inspect(|x| println!("  kept:   {x}"))
        .sum();
    println!("total {total}");
}
Output
in:       4
  kept:   4
in:       -2
in:       7
  kept:   7
in:       -5
in:       3
  kept:   3
total 14

24. peekable: look ahead without consuming

peekable() adds a peek() method that shows the next item without taking it. Parsers use it constantly.

Rust
fn main() {
    let input = "aaabccdddd";
    let mut chars = input.chars().peekable();
    let mut encoded = String::new();
    while let Some(c) = chars.next() {
        let mut run = 1;
        while chars.peek() == Some(&c) {
            chars.next();
            run += 1;
        }
        encoded.push_str(&format!("{run}{c}"));
    }
    println!("{input} -> {encoded}");
}
Output
aaabccdddd -> 3a1b2c4d

This is run-length encoding. next_if shortens the inner loop further: while chars.next_if_eq(&c).is_some() { run += 1; }.

25. cycle: repeat forever

cycle repeats an iterator endlessly. Pair it with zip or take so it ends.

Rust
fn main() {
    let workers = ["w1", "w2", "w3"];
    let jobs = ["resize", "encode", "thumbnail", "upload", "notify"];
    for (job, worker) in jobs.iter().zip(workers.iter().cycle()) {
        println!("{job:<10} -> {worker}");
    }
    let stripes: String = ['#', '.'].iter().cycle().take(9).collect();
    println!("{stripes}");
}
Output
resize     -> w1
encode     -> w2
thumbnail  -> w3
upload     -> w1
notify     -> w2
#.#.#.#.#

This round-robin assignment is a common load-balancing strategy.

26. last and nth

Rust
fn main() {
    let versions = ["1.0.0", "1.1.0", "1.2.3", "2.0.0-beta"];
    println!("{:?}", versions.iter().last());
    println!("{:?}", versions.iter().nth(1));
    println!("{:?}", versions.iter().nth(10));
    let mut it = versions.iter();
    it.nth(1); // consumes items 0 and 1
    println!("after nth(1): {:?}", it.next());
}
Output
Some("2.0.0-beta")
Some("1.1.0")
None
after nth(1): Some("1.2.3")

27. chunk_by: group consecutive items

Slices have chunk_by, which groups consecutive items for which a closure returns true. It's perfect for already-sorted data or runs in time series.

Rust
fn main() {
    let log_levels = ["INFO", "INFO", "WARN", "ERROR", "ERROR", "ERROR", "INFO"];
    let runs: Vec<(&str, usize)> = log_levels.chunk_by(|a, b| a == b).map(|run| (run[0], run.len())).collect();
    println!("{:?}", runs);

    let temps = [18, 19, 21, 25, 26, 24, 17, 16];
    let rising: Vec<&[i32]> = temps.chunk_by(|a, b| b > a).collect();
    println!("{:?}", rising);
}
Output
[("INFO", 2), ("WARN", 1), ("ERROR", 3), ("INFO", 1)]
[[18, 19, 21, 25, 26], [24], [17], [16]]

For grouping all items by a key regardless of order, use a HashMap (HashMap), the equivalent of Collectors.groupingBy.

28. Removing duplicates

Iterators have no distinct. The idiomatic options: sort + dedup on a Vec when order doesn't matter, or a HashSet to keep first occurrences in order.

Rust
use std::collections::HashSet;

fn main() {
    let tags = vec!["rust", "java", "rust", "go", "java", "zig"];

    let mut sorted_unique = tags.clone();
    sorted_unique.sort();
    sorted_unique.dedup();
    println!("{:?}", sorted_unique);

    let mut seen = HashSet::new();
    let first_seen_order: Vec<&str> = tags.iter().copied().filter(|t| seen.insert(*t)).collect();
    println!("{:?}", first_seen_order);
}
Output
["go", "java", "rust", "zig"]
["rust", "java", "go", "zig"]

HashSet::insert returns true only the first time a value is added, which makes it a perfect filter predicate.

29. Sorting inside a pipeline

Iterators can't be sorted, because sorting needs all items at once. Collect first, then sort. Sorting by a derived key uses sort_by_key, or sort_by_cached_key when the key is expensive.

Rust
fn main() {
    let files = ["report_10.pdf", "report_2.pdf", "report_1.pdf", "summary.pdf"];
    let mut by_number: Vec<&str> = files.iter().copied().filter(|f| f.starts_with("report_")).collect();
    by_number.sort_by_key(|f| f.trim_start_matches("report_").trim_end_matches(".pdf").parse::<u32>().unwrap_or(0));
    println!("{:?}", by_number);

    let mut words = vec!["banana", "Apple", "cherry"];
    words.sort_by_cached_key(|w| w.to_lowercase());
    println!("{:?}", words);
}
Output
["report_1.pdf", "report_2.pdf", "report_10.pdf"]
["Apple", "banana", "cherry"]

30. Joining into a String

Rust
fn main() {
    let tags = ["rust", "iterators", "cookbook"];
    println!("{}", tags.join(", "));
    let hashtags: String = tags.iter().map(|t| format!("#{t}")).collect::<Vec<_>>().join(" ");
    println!("{hashtags}");
    let sql_list = (1..=4).map(|id| id.to_string()).collect::<Vec<_>>().join(",");
    println!("WHERE id IN ({sql_list})");
}
Output
rust, iterators, cookbook
#rust #iterators #cookbook
WHERE id IN (1,2,3,4)

31. Realistic example: an access-log report

Several adapters combined to analyze web server logs.

Rust
fn main() {
    let log = "\
GET /api/users 200 45
GET /api/users 200 51
POST /api/orders 201 180
GET /api/orders/9 404 12
GET /static/app.js 200 3
POST /api/orders 500 950
GET /api/users 200 48";

    let entries: Vec<(&str, &str, u16, u32)> = log
        .lines()
        .filter_map(|line| {
            let mut p = line.split_whitespace();
            Some((p.next()?, p.next()?, p.next()?.parse().ok()?, p.next()?.parse().ok()?))
        })
        .collect();

    let api_calls = entries.iter().filter(|e| e.1.starts_with("/api")).count();
    let errors: Vec<String> = entries.iter().filter(|e| e.2 >= 400).map(|e| format!("{} {} -> {}", e.0, e.1, e.2)).collect();
    let avg_ms = entries.iter().map(|e| e.3 as f64).sum::<f64>() / entries.len() as f64;
    let slowest = entries.iter().max_by_key(|e| e.3).map(|e| (e.1, e.3));
    let error_rate = errors.len() as f64 * 100.0 / entries.len() as f64;

    println!("{} requests, {api_calls} to the API", entries.len());
    println!("average {avg_ms:.1} ms, slowest {:?}", slowest);
    println!("error rate {error_rate:.1}%: {:?}", errors);
}
Output
7 requests, 6 to the API
average 184.1 ms, slowest Some(("/api/orders", 950))
error rate 28.6%: ["GET /api/orders/9 -> 404", "POST /api/orders -> 500"]

32. Realistic example: a leaderboard with ranks and ties

Rust
fn main() {
    let mut scores = vec![("ana", 980), ("ben", 1_200), ("cy", 980), ("dee", 750), ("eve", 1_200)];
    scores.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));

    let ranked: Vec<(usize, &str, u32)> = scores
        .iter()
        .enumerate()
        .scan((0, None), |(rank, prev_score), (i, (name, score))| {
            if *prev_score != Some(*score) {
                *rank = i + 1; // ties share a rank
                *prev_score = Some(*score);
            }
            Some((*rank, *name, *score))
        })
        .collect();

    for (rank, name, score) in &ranked {
        println!("#{rank} {name:<4} {score}");
    }
}
Output
#1 ben  1200
#1 eve  1200
#3 ana  980
#3 cy   980
#5 dee  750

33. Realistic example: batch processing with chunks and totals

Rust
fn main() {
    let user_ids: Vec<u32> = (1..=11).collect();
    let batch_size = 4;
    let (sent, batches) = user_ids
        .chunks(batch_size)
        .enumerate()
        .map(|(i, batch)| {
            let ids = batch.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
            println!("batch {}: POST /notify ids={ids}", i + 1);
            batch.len()
        })
        .fold((0, 0), |(sent, batches), n| (sent + n, batches + 1));
    println!("notified {sent} users in {batches} batches");
}
Output
batch 1: POST /notify ids=1,2,3,4
batch 2: POST /notify ids=5,6,7,8
batch 3: POST /notify ids=9,10,11
notified 11 users in 3 batches

34. Realistic example: detecting anomalies with windows

Slices have windows(n), overlapping groups of n items. Comparing each reading with the average of the previous ones flags sudden spikes.

Rust
fn main() {
    let requests_per_min = [120, 118, 125, 122, 480, 130, 127, 124, 20, 121];
    let window = 3;
    let anomalies: Vec<(usize, u32, f64)> = requests_per_min
        .windows(window + 1)
        .enumerate()
        .filter_map(|(i, w)| {
            let baseline = w[..window].iter().sum::<u32>() as f64 / window as f64;
            let current = w[window];
            let deviation = (current as f64 - baseline).abs() / baseline;
            (deviation > 0.5).then_some((i + window, current, baseline))
        })
        .collect();
    for (minute, value, baseline) in anomalies {
        println!("minute {minute}: {value} req/min vs baseline {baseline:.0}");
    }
}
Output
minute 4: 480 req/min vs baseline 122
minute 8: 20 req/min vs baseline 127

condition.then_some(value) returns Some(value) if the condition is true and None otherwise, which fits filter_map neatly.

Summary

  • Transform: map, filter, filter_map, flat_map, flatten, enumerate, zip, chain, inspect.
  • Select: take, skip, take_while, skip_while, step_by, rev, peekable, cycle.
  • Reduce: fold, reduce, scan (running values), sum, product, count, min/max and their _by_key/_by forms.
  • Search: find, find_map, position, rposition, any, all, last, nth.
  • Split and collect: partition, unzip, collect into any collection, and join on slices of strings.
  • Slice helpers complete the toolbox: chunks, windows, chunk_by, sort_by_key, dedup.

Next: Custom Iterators.