Skip to content

zip and enumerate

Level: 101 → 201 · working knowledge

One line: zip walks two sequences as one sequence of pairs and stops when the shorter one runs out; enumerate numbers whatever reaches it — and the order you write them in decides both the shape of the pattern and what the number counts.

fn main() {
    let ids = vec![10, 20, 30];
    let tags = vec!["a", "b", "c"];

    for (i, (id, tag)) in ids.iter().zip(tags.iter()).enumerate() {
        println!("{i}: {id} => {tag}");  // 0: 10 => a, then 1: 20 => b, 2: 30 => c
    }
}

(i, (id, tag)) is the whole syntactic cost. zip produces an (id, tag) pair, enumerate wraps each item in (index, item), and the pattern unwraps both layers in one go. Nothing indexes anything: there is no v1[i], so there is no bounds check and no way to be one row off.

Two collections can be joined two ways, and this is the side-by-side one — row i of each becomes one pair, and the result is as long as the shorter input. Joining them end-to-end is a different pair of tools: Vec::append to move one onto the other, or chain to walk both without building anything.

The two orders are not the same pattern

ids.iter().zip(tags.iter()).enumerate()   // (i, (id, tag))
ids.iter().enumerate().zip(tags.iter())   // ((i, id), tag)

Same six values, nested the other way round. Pick the first — the index belongs to the row, not to the left-hand column — and reach for the second only when something between the two calls needs the index already attached.

Verified output of zip_and_enumerate.rs — regenerated by tools/run_examples.py, never hand-typed.

1. zip pairs, enumerate numbers
   ["0: 10 => a", "1: 20 => b", "2: 30 => c"]

2. The other order gives the same data in a different shape
   .zip().enumerate()  [(0, (10, "a")), (1, (20, "b")), (2, (30, "c"))]
   .enumerate().zip()  [((0, 10), "a"), ((1, 20), "b"), ((2, 30), "c")]
   Same six values. The pattern that destructures them is not the
   same: (i, (id, tag)) above, ((i, id), tag) below.

3. zip stops at the shorter side, and says nothing
   4 ids, 3 tags -> 3 pairs
   [(10, "a"), (20, "b"), (30, "c")]
   40 is gone. No panic, no warning, no `Result` — the loop just
   runs one row short, and every count downstream is wrong by one.

4. enumerate numbers what reaches IT, not the source
   .filter().enumerate()  [(0, 10), (1, 30), (2, 50)]
   .enumerate().filter()  [(0, 10), (2, 30), (4, 50)]
   Put enumerate first for a position in the ORIGINAL sequence;
   last for a row number in the output.

5. zip takes an IntoIterator, not an iterator
   .zip(&owned)       [(10, 100), (20, 200), (30, 300)]   owned still usable: [100, 200, 300]
   .zip(owned)        [(10, 100), (20, 200), (30, 300)]   owned is gone: E0382 on any further use
   The pairs print the same; the item types do not. .zip(&owned)
   yields (&i32, &i32), .zip(owned) yields (&i32, i32).

6. The index is a usize
   [0, 20, 60]
   Drop the `as i32` and this stops compiling — but WHERE rustc
   points depends on the annotation. `let ids: Vec<i32>` gets you
   `cannot multiply usize by &i32`; without it, `ids` is inferred
   as Vec<usize> and the error lands on collect, blaming
   FromIterator and never mentioning the multiply.

zip stops at the shorter side, and says nothing

Four ids against three tags produce three pairs. 40 is not an error, not a None, not a warning: it is simply never visited, and the only trace is a count nobody printed.

That is the trap this page exists for, and it is worse than an out-of-bounds panic, because a panic tells you. A zip over two collections that drifted apart returns a shorter answer that looks entirely well-formed.

If the two lengths are supposed to match, say so where the assumption lives:

assert_eq!(ids.len(), tags.len(), "one tag per id");
for (i, (id, tag)) in ids.iter().zip(tags.iter()).enumerate() {
    println!("{i}: {id} => {tag}");  // 0: 10 => a, then 1: 20 => b, 2: 30 => c
}

The deeper fix is to stop having two collections. Two Vecs that must stay the same length are one Vec of a struct or a tuple with the invariant not yet written down — which is what a record is, in memory, and the reason that page's kata is about desyncing exactly this.

enumerate numbers what reaches it

xs.iter().filter(|x| **x % 10 == 0).enumerate()   // 0, 1, 2 — row number in the output
xs.iter().enumerate().filter(|(_, x)| **x % 10 == 0)   // 0, 2, 4 — position in the source

Both are right answers to different questions, and neither is a default. Put enumerate first when the number means where this came from — a line number, an original rank, an index back into the source. Put it last when it means which row of the report this is.

The bug this produces is quiet in the same way zip's is: the numbers are consecutive and start at zero, so nothing looks wrong until someone uses one to index back into the collection that was filtered.

zip takes an IntoIterator, not an iterator

fn zip<U>(self, other: U) -> Zip<Self, U::IntoIter>
where
    Self: Sized,
    U: IntoIterator,

So .zip(tags.iter()), .zip(&tags) and .zip(tags) all compile, and the last one is not the same program — it moves tags into the chain, and the next line that mentions it is E0382:

Abridged — real rustc output, without the trailing help block
error[E0382]: borrow of moved value: `tags`
 --> zip_moved_it.rs:5:26
  |
3 |     let tags = vec!["a", "b", "c"];
  |         ---- move occurs because `tags` has type `Vec<&str>`, which does not implement the `Copy` trait
4 |     let pairs: Vec<(&i32, &str)> = ids.iter().zip(tags).collect();
  |                                                   ---- value moved here
5 |     println!("{pairs:?} {tags:?}");
  |                          ^^^^ value borrowed here after move

The item type changes too: .zip(&tags) yields (&i32, &&str), .zip(tags) yields (&i32, &str). That is the three doors again, chosen by the argument rather than by a & on a loop.

The index is a usize, and dropping the cast moves the error

enumerate counts in usize, so i as i32 * n needs its cast. Remove it and what rustc says depends on something that is nowhere near the multiplication:

let ids: Vec<i32> = vec![10, 20, 30];              // annotated
let ids = vec![10, 20, 30];                        // inferred

With the annotation you get the honest complaint, error[E0277]: cannot multiply usize by &i32, alongside an E0308 at the same span. Without it, rustc never mentions the multiply at all — i * n is the only constraint on the literals, so the vector is inferred as Vec<usize> and the failure surfaces at the far end of the chain:

Abridged — real rustc output, first block only
error[E0277]: a value of type `Vec<i32>` cannot be built from an iterator over elements of type `usize`
 --> usize_times_i32.rs:3:71
  |
3 |     let scaled: Vec<i32> = ids.iter().enumerate().map(|(i, n)| i * n).collect();
  |                                                                       ^^^^^^^ value of type `Vec<i32>` cannot be built from `std::iter::Iterator<Item=usize>`

The error names collect, FromIterator and Vec<i32>, and the mistake is a missing as i32 twenty columns to the left. Worth recognising, because the shape recurs across the whole library: an annotation does not add a constraint so much as decide which end of the chain the conflict is reported at.

If you are coming from another language

Python. Both functions are the ones you already use, and one of them changed after you learned it. zip truncates identically — that part transfers exactly — but Python 3.10 added zip(a, b, strict=True), which raises ValueError: zip() argument 2 is shorter than argument 1. std's zip has no strict and no companion that returns a Result: the std answer is the assert_eq! above, and the crate answer is itertools::zip_eq, which panics on a length mismatch. So the Python habit worth carrying over is the newer one — if you have started writing strict=True by reflex, that reflex has no keyword to land on here and has to become an assertion.

enumerate is the same function with one argument missing. Python's takes a start: enumerate(xs, start=1) is how you number a report from 1. Rust's takes nothing, so the two ways to write it are .enumerate().map(|(i, x)| (i + 1, x)) or, closer to what it means, (1..).zip(xs) — an infinite counter zipped against a finite sequence, which terminates for the reason this whole page is about. That is not a trick: std's own zip docs raise it, noting that "zipping with (0..) can look a lot like enumerate".

The nesting is identical in both languages: Python's enumerate(zip(a, b)) yields (i, (a, b)) and unpacks as for i, (x, y) in ..., which is the same shape as the Rust pattern down to the parentheses. What is new is the &. for (i, (id, tag)) over .iter() binds references, so id is &i32 — arithmetic still works, because std implements Add for &i32, but putting one into a Vec<i32> needs *id, a & in the pattern, or .copied() upstream. Python has no such distinction and no such line.

ABAP. There is no zip, and the absence is structural rather than a missing builtin: internal tables are walked one at a time, so pairing two of them means LOOP AT itab1 INTO wa1 with a READ TABLE itab2 INTO wa2 INDEX sy-tabix inside. sy-tabix is enumerate — the loop's running index, handed to you whether you asked for it or not. Two differences are worth holding onto. First, that inner READ TABLE is the bounds check: past the end it sets sy-subrc = 4 and leaves wa2 untouched, so a length mismatch is detectable and the discipline is to check sy-subrc every time, exactly as with any other read. Rust's zip inverts this — it cannot be one row off, because there is no index, but it will also never tell you the tables were different lengths. Second, sy-tabix is a system field shared with everything else in the loop body: a nested LOOP or READ TABLE overwrites it, which is why the ABAP habit is to copy it into a local variable immediately. Rust's index is a binding in the pattern, so nothing can reach in and change it — the whole class of bug does not exist here.

Practice

Five pairings, and the row that vanishes. Take two vectors each time and write five loops, using zip throughout and enumerate wherever a number is wanted: print the index and both elements; build a Vec<String> of "{i}: {a} and {b}"; print the sum of each pair with its index; keep only the pairs whose sum is even, collecting a Vec<(i32, i32)>; and multiply each pair by its index into a Vec<i32>.

Two of those five will not compile the way you first write them, for two unrelated reasons — one about &, one about usize — and the fix for the first has three forms worth writing out and comparing.

Then the prediction. Re-run the even-sum filter with an eight-element vector against a six-element one, and before running it, write down how many pairs get tested and which inputs are never looked at. Say what you would add to the program so that the answer is not silently wrong.

Solution

zip_and_enumerate_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

//! Kata solution: five pairings over two vectors, and the row that vanishes.
//!
//!   rustc --edition 2024 zip_and_enumerate_kata.rs -o /tmp/zaek && /tmp/zaek

fn main() {
    println!("1. Print the index and both elements");
    let ints = vec![10, 20, 30, 40];
    let floats = vec![1.1, 2.2, 3.3, 4.4];
    for (i, (n, f)) in ints.iter().zip(floats.iter()).enumerate() {
        println!("   Index: {i}, Integer: {n}, Float: {f}");
    }

    println!();
    println!("2. Concatenate into a new Vec<String>");
    let left = vec!["hello", "rust", "world"];
    let right = vec!["goodbye", "c++", "earth"];
    let joined: Vec<String> = left
        .iter()
        .zip(right.iter())
        .enumerate()
        .map(|(i, (a, b))| format!("{i}: {a} and {b}"))
        .collect();
    println!("   {joined:?}");

    println!();
    println!("3. Sum each pair");
    let a = vec![5, 10, 15];
    let b = vec![3, 6, 9];
    for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
        // x and y are &i32. `x + y` compiles anyway: std implements Add for
        // &i32, so the references add without a deref in sight.
        println!("   Index: {i}, Sum: {}", x + y);
    }

    println!();
    println!("4. Keep the pairs whose sum is even — three ways to get past the &");
    let p = vec![2, 2, 2, 3, 4, 5];
    let q = vec![4, 8, 1, 2, 2, 2];

    // (a) deref at the push site
    let mut deref_at_push: Vec<(i32, i32)> = Vec::new();
    for (x, y) in p.iter().zip(q.iter()) {
        if (x + y) % 2 == 0 {
            deref_at_push.push((*x, *y));
        }
    }

    // (b) deref in the pattern — the & goes on the left of the =
    let mut deref_in_pattern: Vec<(i32, i32)> = Vec::new();
    for (&x, &y) in p.iter().zip(q.iter()) {
        if (x + y) % 2 == 0 {
            deref_in_pattern.push((x, y));
        }
    }

    // (c) copied() upstream, so the chain never sees a reference at all
    let copied: Vec<(i32, i32)> = p
        .iter()
        .copied()
        .zip(q.iter().copied())
        .filter(|(x, y)| (x + y) % 2 == 0)
        .collect();

    println!("   (a) deref at push     {deref_at_push:?}");
    println!("   (b) deref in pattern  {deref_in_pattern:?}");
    println!("   (c) .copied() first   {copied:?}");
    println!("   Identical. (c) is the one to reach for in a chain: filter hands");
    println!("   its closure a reference to the item, so a chain over references");
    println!("   makes the pattern &(x, y) where (b) would have written (&x, &y).");

    println!();
    println!("5. Multiply each pair by its index");
    let u = vec![2, 4, 6];
    let v = vec![3, 5, 7];
    let transformed: Vec<i32> = u
        .iter()
        .zip(v.iter())
        .enumerate()
        .map(|(i, (x, y))| i as i32 * x * y)
        .collect();
    println!("   {transformed:?}");
    println!("   `i as i32` is not decoration: enumerate counts in usize, and");
    println!("   there is no silent widening to i32. Drop the cast and what");
    println!("   rustc says depends on whether the Vec was annotated — see the");
    println!("   lesson: unannotated, it does not mention the multiply at all.");

    println!();
    println!("6. The prediction: one vector is longer than the other");
    let long = vec![2, 2, 2, 3, 4, 5, 6, 8];
    let short = vec![4, 8, 1, 2, 2, 2];
    let even: Vec<(i32, i32)> = long
        .iter()
        .copied()
        .zip(short.iter().copied())
        .filter(|(x, y)| (x + y) % 2 == 0)
        .collect();
    println!("   long has {} items, short has {}", long.len(), short.len());
    println!("   pairs examined: {}", long.iter().zip(short.iter()).count());
    println!("   kept: {even:?}");
    println!("   6 and 8 were never tested. zip ended the sequence when the");
    println!("   shorter side ran out, and the only evidence is a count nobody");
    println!("   printed. If both sides are supposed to be the same length, say");
    println!("   so before the loop:");

    if long.len() != short.len() {
        println!("   assert_eq! would fail here: {} != {}", long.len(), short.len());
    }
}

Verified output of zip_and_enumerate_kata.rs — regenerated by tools/run_examples.py, never hand-typed.

1. Print the index and both elements
   Index: 0, Integer: 10, Float: 1.1
   Index: 1, Integer: 20, Float: 2.2
   Index: 2, Integer: 30, Float: 3.3
   Index: 3, Integer: 40, Float: 4.4

2. Concatenate into a new Vec<String>
   ["0: hello and goodbye", "1: rust and c++", "2: world and earth"]

3. Sum each pair
   Index: 0, Sum: 8
   Index: 1, Sum: 16
   Index: 2, Sum: 24

4. Keep the pairs whose sum is even — three ways to get past the &
   (a) deref at push     [(2, 4), (2, 8), (4, 2)]
   (b) deref in pattern  [(2, 4), (2, 8), (4, 2)]
   (c) .copied() first   [(2, 4), (2, 8), (4, 2)]
   Identical. (c) is the one to reach for in a chain: filter hands
   its closure a reference to the item, so a chain over references
   makes the pattern &(x, y) where (b) would have written (&x, &y).

5. Multiply each pair by its index
   [0, 20, 84]
   `i as i32` is not decoration: enumerate counts in usize, and
   there is no silent widening to i32. Drop the cast and what
   rustc says depends on whether the Vec was annotated — see the
   lesson: unannotated, it does not mention the multiply at all.

6. The prediction: one vector is longer than the other
   long has 8 items, short has 6
   pairs examined: 6
   kept: [(2, 4), (2, 8), (4, 2)]
   6 and 8 were never tested. zip ended the sequence when the
   shorter side ran out, and the only evidence is a count nobody
   printed. If both sides are supposed to be the same length, say
   so before the loop:
   assert_eq! would fail here: 8 != 6

See also

  • Adapters by job — the other nineteen, including chain for the end-to-end join and unzip, which is this page run backwards
  • Vec::append — the other meaning of merge two vectors: one emptied onto the end of the other
  • Iterators are lazy — including the measured proof that zip pulls the left side before it discovers the right one is empty, so the longer side is always pulled one extra time
  • iter, iter_mut, into_iter — the three doors the zip argument is choosing between
  • DoubleEndedIterator and ExactSizeIterator — why .enumerate().rev() and .rev().enumerate() number the same rows differently
  • What is a record, in memory? — the fix for two Vecs that have to stay the same length
  • What a warning is asking — including the mut on a vector you only ever read, which is what let mut v1 = vec![…] earns when the loop just walks it

Sources

Iterator::zip and Iterator::enumerate — the signatures quoted above, and zip's own note that it stops when either side returns None. Python's side: zip, whose strict parameter arrived in 3.10.

Po polsku

Dwie funkcje, dwa zadania. zip sparuje dwie sekwencje w jedną sekwencję par, a enumerate ponumeruje to, co do niego dotrze. Razem dają wzorzec (i, (id, tag)) — i to jest cały koszt składniowy, bo nic tu niczego nie indeksuje: nie ma v1[i], więc nie ma sprawdzania zakresu ani możliwości pomylenia się o jeden wiersz. Kolejność wywołań decyduje o kształcie wzorca: .zip().enumerate() daje (i, (id, tag)), a .enumerate().zip()((i, id), tag). Te same sześć wartości, zagnieżdżone odwrotnie.

Pułapka, dla której ta strona istnieje: zip kończy na krótszej stronie i nie mówi o tym ani słowa. Cztery identyfikatory i trzy etykiety dają trzy pary; 40 nie jest błędem, nie jest None, nie jest ostrzeżeniem — po prostu nikt go nie odwiedził. To gorsze niż panika przy wyjściu poza zakres, bo panika przynajmniej informuje. Jeśli obie długości mają być równe, napisz to tam, gdzie mieszka założenie: assert_eq!(ids.len(), tags.len(), "one tag per id");. Głębsze rozwiązanie jest inne — dwa wektory, które muszą mieć tę samą długość, to jeden wektor struktur z niezapisanym jeszcze niezmiennikiem (invariant).

Drugie ciche zachowanie dotyczy numeracji: enumerate numeruje to, co do niego dociera, a nie źródło. .filter().enumerate() da 0, 1, 2 — numer wiersza w wyniku; .enumerate().filter() da 0, 2, 4 — pozycję w oryginale. Obie odpowiedzi są poprawne, tylko na różne pytania, i żadna nie jest domyślna. Numery w obu przypadkach są kolejne i zaczynają się od zera, więc nic nie wygląda podejrzanie aż do chwili, gdy ktoś użyje takiego numeru do zaindeksowania kolekcji, która została przefiltrowana.

Osobie znającej Pythona obie funkcje są znajome, ale jedna zmieniła się po nauce: Python 3.10 dodał zip(a, b, strict=True), które rzuca ValueError. W Ruście nie ma odpowiednika strict — zostaje assert_eq! albo itertools::zip_eq, które panikuje. Pythonowy enumerate przyjmuje start, rustowy nie przyjmuje niczego, więc numerowanie od jedynki pisze się jako (1..).zip(xs) — nieskończony licznik sparowany ze skończoną sekwencją, co kończy się właśnie z powodu opisanego wyżej (sama dokumentacja zip zwraca na to uwagę: sparowanie z (0..) wygląda niemal jak enumerate). Nowy jest natomiast &: pętla po .iter() wiąże referencje, więc id ma typ &i32. Arytmetyka i tak zadziała, bo Add jest zaimplementowane dla &i32, ale żeby wstawić taką wartość do Vec<i32>, trzeba *id, & we wzorcu albo .copied() wcześniej w łańcuchu.

Na koniec drobiazg o typie licznika, wart zapamiętania szerzej niż ta strona. enumerate liczy w usize, więc i as i32 * n potrzebuje rzutowania. Usuń je, a treść błędu zależy od czegoś, co leży zupełnie gdzie indziej: z adnotacją let ids: Vec<i32> dostaniesz uczciwe cannot multiply usize by &i32, a bez niej kompilator w ogóle nie wspomni o mnożeniu — wywnioskuje Vec<usize> i zgłosi błąd dopiero przy collect, mówiąc o FromIterator. Adnotacja typu nie tyle dokłada ograniczenie, ile decyduje, na którym końcu łańcucha zostanie zgłoszony konflikt.

Szukaj po polsku: iteratory w Ruscie · adaptery iteratorów · dopasowanie wzorca do krotki · rust zip enumerate · rust zip different lengths · python zip strict