#124 May 7, 2026

124. Iterator::cycle — Round-Robin Without the Modulo Math

Round-robin assignment usually shows up as things[i % workers.len()] — fine until the index gets clever, the slice gets reordered, or the source isn’t even indexable. Iterator::cycle turns any Clone iterator into an infinite one, and the modulo dance disappears.

The textbook version: distribute jobs across a fixed pool of workers. Indexing works, but you’re carrying the index and the modulo around just to walk in a circle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let workers = ["alice", "bob", "carol"];
let jobs = ["build", "test", "lint", "deploy", "notify"];

// Index-and-modulo: works, but the math is doing the iterating for you.
let mut assigned: Vec<(&str, &str)> = Vec::new();
for (i, job) in jobs.iter().copied().enumerate() {
    assigned.push((job, workers[i % workers.len()]));
}

assert_eq!(
    assigned,
    vec![
        ("build",  "alice"),
        ("test",   "bob"),
        ("lint",   "carol"),
        ("deploy", "alice"),
        ("notify", "bob"),
    ],
);

Swap the modulo for cycle and the loop tells you exactly what it’s doing — pull the next worker, forever:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let workers = ["alice", "bob", "carol"];
let jobs = ["build", "test", "lint", "deploy", "notify"];

let assigned: Vec<(&str, &str)> = jobs
    .iter()
    .copied()
    .zip(workers.iter().copied().cycle())
    .collect();

assert_eq!(
    assigned,
    vec![
        ("build",  "alice"),
        ("test",   "bob"),
        ("lint",   "carol"),
        ("deploy", "alice"),
        ("notify", "bob"),
    ],
);

The trick is zip — zipping a finite iterator with an infinite one stops as soon as the finite side runs out, so you never have to bound cycle yourself. No off-by-one, no bookkeeping for “did I already use this worker?”.

It also composes with take when you want a fixed-length output and the source is the short one:

1
2
3
4
5
let pattern = [1, 2, 3];

let twelve: Vec<i32> = pattern.iter().copied().cycle().take(12).collect();

assert_eq!(twelve, vec![1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]);

A handy companion is enumerate — when you want the round-robin and the original index together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
let colors = ["red", "green", "blue"];
let rows = ["one", "two", "three", "four", "five"];

let painted: Vec<(usize, &str, &str)> = rows
    .iter()
    .copied()
    .zip(colors.iter().copied().cycle())
    .enumerate()
    .map(|(i, (row, color))| (i, row, color))
    .collect();

assert_eq!(
    painted,
    vec![
        (0, "one",   "red"),
        (1, "two",   "green"),
        (2, "three", "blue"),
        (3, "four",  "red"),
        (4, "five",  "green"),
    ],
);

Two things worth knowing. cycle requires the underlying iterator to be Clone — it remembers the original and starts over each time it’s exhausted, which means it’ll panic or loop forever on an empty iterator depending on what you do with it (zip is safe — empty side wins; bare .next() would just return None forever). And it’s lazy: nothing repeats until the consumer pulls another item, so pairing it with a finite iterator costs nothing extra.

Stable since Rust 1.0 — one of those iterator adapters that makes the modulo operator feel like the wrong tool the moment you remember it exists.

← Previous 123. BTreeMap::pop_first — A Sorted Map That Doubles as a Priority Queue