902-iterator-enumerate — Iterator Enumerate
Tutorial
The Problem
Algorithms that process both element values and their positions — numbering output lines, finding the index of a match, filtering by even/odd positions — need both the index and the value simultaneously. The naive approach uses a separate mutable counter variable, which is error-prone and verbose. Python's enumerate() and OCaml's List.mapi solve this idiomatically. Rust's .enumerate() wraps any iterator to yield (usize, T) pairs, composing cleanly with .filter(), .find(), and .map() without mutable counter variables.
🎯 Learning Outcomes
.enumerate() to add zero-based indices to any iterator.enumerate().filter(|(i, _)| ...).enumerate().find(...).enumerate().map(|(i, s)| format!("{}. {}", i+1, s))List.mapi and Array.iteriCode Example
let fruits = ["apple", "banana", "cherry"];
// Loop with index
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// Filter by index — stays in the lazy pipeline
let evens: Vec<_> = fruits.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(_, v)| *v)
.collect();
// Map with index
let numbered: Vec<String> = fruits.iter()
.enumerate()
.map(|(i, f)| format!("{}. {}", i + 1, f))
.collect();Key Differences
enumerate() composes with all other adapters; OCaml requires separate mapi/filteri/find_index functions per operation..enumerate() is lazy; OCaml List.mapi is eager and allocates..enumerate().find().map(|(i, _)| i) is idiomatic; OCaml lacked find_index until 5.1 — required manual recursion.i+1 for human-readable 1-based numbering.OCaml Approach
List.mapi: (int -> 'a -> 'b) -> 'a list -> 'b list maps with index. List.iteri: (int -> 'a -> unit) -> 'a list -> unit iterates with index. Finding an index requires List.find_index (OCaml 5.1) or manual implementation: let find_index pred xs = let rec go i = function | [] -> None | x :: rest -> if pred x then Some i else go (i+1) rest in go 0 xs. Array: Array.iteri f arr provides direct indexed access. List.filteri: (int -> 'a -> bool) -> 'a list -> 'a list is the filter-by-index equivalent.
Full Source
#![allow(clippy::all)]
//! 258. Index-value pairs with enumerate()
//!
//! `enumerate()` adds a zero-based index to every iterator element,
//! turning `Iterator<Item=T>` into `Iterator<Item=(usize, T)>`.
//! This integrates cleanly with filter, map, find, and other adapters
//! without needing a mutable counter variable.
/// Return only elements at even indices (0, 2, 4, ...).
///
/// Takes &[T] — borrows the slice, no allocation needed for the input.
pub fn even_indexed<T>(items: &[T]) -> Vec<&T> {
items
.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(_, v)| v)
.collect()
}
/// Number each element as "1. item", "2. item", ... (1-based).
pub fn number_items(items: &[&str]) -> Vec<String> {
items
.iter()
.enumerate()
.map(|(i, s)| format!("{}. {}", i + 1, s))
.collect()
}
/// Find the index of the first element satisfying the predicate.
pub fn find_index<T, F>(items: &[T], pred: F) -> Option<usize>
where
F: Fn(&T) -> bool,
{
items
.iter()
.enumerate()
.find(|(_, v)| pred(v))
.map(|(i, _)| i)
}
/// Return (index, value) pairs where the value satisfies the predicate.
pub fn indexed_filter<T, F>(items: &[T], pred: F) -> Vec<(usize, &T)>
where
F: Fn(&T) -> bool,
{
items.iter().enumerate().filter(|(_, v)| pred(v)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_even_indexed_empty() {
let empty: &[i32] = &[];
assert_eq!(even_indexed(empty), Vec::<&i32>::new());
}
#[test]
fn test_even_indexed_single() {
assert_eq!(even_indexed(&[42]), vec![&42]);
}
#[test]
fn test_even_indexed_multiple() {
let fruits = ["apple", "banana", "cherry", "date"];
assert_eq!(even_indexed(&fruits), vec![&"apple", &"cherry"]);
}
#[test]
fn test_even_indexed_two_elements() {
let items = [10, 20];
assert_eq!(even_indexed(&items), vec![&10]);
}
#[test]
fn test_number_items_empty() {
assert_eq!(number_items(&[]), Vec::<String>::new());
}
#[test]
fn test_number_items_single() {
assert_eq!(number_items(&["only"]), vec!["1. only"]);
}
#[test]
fn test_number_items_multiple() {
let items = ["apple", "banana", "cherry"];
assert_eq!(
number_items(&items),
vec!["1. apple", "2. banana", "3. cherry"]
);
}
#[test]
fn test_find_index_present() {
let fruits = ["apple", "banana", "cherry"];
assert_eq!(find_index(&fruits, |f| f.starts_with('c')), Some(2));
}
#[test]
fn test_find_index_absent() {
let fruits = ["apple", "banana", "cherry"];
assert_eq!(find_index(&fruits, |f| f.starts_with('z')), None);
}
#[test]
fn test_find_index_first_match() {
let nums = [3, 7, 2, 8, 4];
// First even number is at index 2
assert_eq!(find_index(&nums, |n| n % 2 == 0), Some(2));
}
#[test]
fn test_indexed_filter_multiple_matches() {
let nums = [1, 2, 3, 4, 5, 6];
let evens = indexed_filter(&nums, |n| n % 2 == 0);
assert_eq!(evens, vec![(1, &2), (3, &4), (5, &6)]);
}
#[test]
fn test_indexed_filter_no_matches() {
let nums = [1, 3, 5];
assert_eq!(indexed_filter(&nums, |n| n % 2 == 0), vec![]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_even_indexed_empty() {
let empty: &[i32] = &[];
assert_eq!(even_indexed(empty), Vec::<&i32>::new());
}
#[test]
fn test_even_indexed_single() {
assert_eq!(even_indexed(&[42]), vec![&42]);
}
#[test]
fn test_even_indexed_multiple() {
let fruits = ["apple", "banana", "cherry", "date"];
assert_eq!(even_indexed(&fruits), vec![&"apple", &"cherry"]);
}
#[test]
fn test_even_indexed_two_elements() {
let items = [10, 20];
assert_eq!(even_indexed(&items), vec![&10]);
}
#[test]
fn test_number_items_empty() {
assert_eq!(number_items(&[]), Vec::<String>::new());
}
#[test]
fn test_number_items_single() {
assert_eq!(number_items(&["only"]), vec!["1. only"]);
}
#[test]
fn test_number_items_multiple() {
let items = ["apple", "banana", "cherry"];
assert_eq!(
number_items(&items),
vec!["1. apple", "2. banana", "3. cherry"]
);
}
#[test]
fn test_find_index_present() {
let fruits = ["apple", "banana", "cherry"];
assert_eq!(find_index(&fruits, |f| f.starts_with('c')), Some(2));
}
#[test]
fn test_find_index_absent() {
let fruits = ["apple", "banana", "cherry"];
assert_eq!(find_index(&fruits, |f| f.starts_with('z')), None);
}
#[test]
fn test_find_index_first_match() {
let nums = [3, 7, 2, 8, 4];
// First even number is at index 2
assert_eq!(find_index(&nums, |n| n % 2 == 0), Some(2));
}
#[test]
fn test_indexed_filter_multiple_matches() {
let nums = [1, 2, 3, 4, 5, 6];
let evens = indexed_filter(&nums, |n| n % 2 == 0);
assert_eq!(evens, vec![(1, &2), (3, &4), (5, &6)]);
}
#[test]
fn test_indexed_filter_no_matches() {
let nums = [1, 3, 5];
assert_eq!(indexed_filter(&nums, |n| n % 2 == 0), vec![]);
}
}
Deep Comparison
OCaml vs Rust: Index-Value Pairs with enumerate()
Side-by-Side Code
OCaml
let fruits = ["apple"; "banana"; "cherry"] in
(* Loop with index *)
List.iteri (fun i fruit ->
Printf.printf "%d: %s\n" i fruit
) fruits;
(* Filter by index *)
let evens_only = List.filteri (fun i _ -> i mod 2 = 0) fruits in
(* Map with index *)
let numbered = List.mapi (fun i name ->
Printf.sprintf "#%d %s" (i + 1) name
) fruits
Rust (idiomatic)
let fruits = ["apple", "banana", "cherry"];
// Loop with index
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// Filter by index — stays in the lazy pipeline
let evens: Vec<_> = fruits.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(_, v)| *v)
.collect();
// Map with index
let numbered: Vec<String> = fruits.iter()
.enumerate()
.map(|(i, f)| format!("{}. {}", i + 1, f))
.collect();
Rust (functional — find with index)
// Find the index of the first element matching a predicate
fn find_index<T, F: Fn(&T) -> bool>(items: &[T], pred: F) -> Option<usize> {
items
.iter()
.enumerate()
.find(|(_, v)| pred(v))
.map(|(i, _)| i)
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Indexed iteration | List.iteri : (int -> 'a -> unit) -> 'a list -> unit | Iterator::enumerate() -> impl Iterator<Item=(usize, T)> |
| Filter by index | List.filteri : (int -> 'a -> bool) -> 'a list -> 'a list | .enumerate().filter(|(i, _)| ...).map(|(_, v)| v) |
| Map with index | List.mapi : (int -> 'a -> 'b) -> 'a list -> 'b list | .enumerate().map(|(i, v)| ...) |
| Find by predicate | List.find_opt : ('a -> bool) -> 'a list -> 'a option | .find(|(_, v)| pred(v)) → Option<(usize, &T)> |
Key Insights
List.iteri, List.filteri, and List.mapi as distinct functions for each indexed operation. Rust exposes a single enumerate() adapter that composes uniformly with the rest of the iterator chain — you don't need a separate API for every combination.List.mapi produces a new list immediately. Rust's .enumerate().map(...) is lazy; nothing is computed until .collect() or a consuming adapter is called. This lets you chain more transformations before paying allocation cost.int (which is the platform-native signed integer) for indices. Rust uses usize — the unsigned, pointer-sized integer — which is the natural choice for collection indices and prevents negative-index bugs at the type level.fun i x -> ...; Rust uses |(i, x)| ... in closure patterns. Rust additionally supports |(i, _)| ... to ignore components, matching OCaml's fun _ x -> ....let mut i = 0; ... i += 1;) is completely replaced. In Rust, .enumerate() works on any iterator — not just slices — so you get index-awareness even when iterating files, channels, or custom iterators.When to Use Each Style
**Use enumerate() in a chain when:** you need the index alongside filtering or mapping — staying lazy avoids an intermediate collect() and keeps the pipeline readable.
**Use enumerate() in a for loop when:** you only need to iterate with side effects (printing, mutating external state) and don't need to produce a new collection.
Exercises
.enumerate() to implement rotate_left(data: &[i32], n: usize) -> Vec<i32> that shifts elements left by n positions.positions_of<T: PartialEq>(data: &[T], target: &T) -> Vec<usize> using enumerate().filter_map().annotate_changes<T: PartialEq + Clone>(data: &[T]) -> Vec<(usize, &T)> that returns (index, value) only for positions where the value changed from the previous element.