894-step-by — Step By, Enumerate, Rev
Tutorial
The Problem
Structured traversal — skipping elements at regular intervals, attaching position indices, or traversing in reverse — arises constantly in data processing. NumPy's arange(start, stop, step), Python's range(start, stop, step), and SQL's ROW_NUMBER() analytic function all serve structured traversal needs. Rust provides three zero-cost adapter methods for these: .step_by(n) for strided access, .enumerate() for index attachment, and .rev() for reversal. These compose cleanly: .enumerate().rev() gives reverse-indexed enumeration, and .step_by(2).enumerate() gives even-indexed positions.
🎯 Learning Outcomes
.step_by(n) to visit every nth element of an iterator.enumerate() to pair elements with zero-based indices.rev() to iterate in reverse over a DoubleEndedIteratorList.filteri, List.mapi, and List.revCode Example
fn every_nth(data: &[i32], n: usize) -> Vec<i32> {
data.iter().step_by(n).copied().collect()
}
fn indexed_filter(data: &[i32], pred: impl Fn(&i32) -> bool) -> Vec<(usize, i32)> {
data.iter().enumerate().filter(|(_, x)| pred(x)).map(|(i, &x)| (i, x)).collect()
}
fn rev_map(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
data.iter().rev().map(|&x| f(x)).collect()
}Key Differences
List.filteri followed by List.mapi allocates two intermediate lists.(0..100).step_by(3) is a built-in lazy range; OCaml requires explicit List.init with stride arithmetic.mapi is also 0-based. Both require +1 for 1-based display..step_by(2).rev() works because StepBy implements DoubleEndedIterator; OCaml requires List.rev before or after filtering.OCaml Approach
OCaml's List.filteri (fun i _ -> i mod n = 0) xs implements step_by. List.mapi (fun i x -> (i, f i x)) xs is enumerate-and-map. List.rev xs reverses. For arrays: Array.iteri f arr provides index with each element. OCaml lacks a generic step_by on lists in the standard library — List.filteri is the idiomatic substitute. For Seq, Seq.filter (fun _ -> ...) with a mutable counter or Seq.zip (Seq.ints 0) provides indexed access.
Full Source
#![allow(clippy::all)]
// Example 894: Step By, Enumerate, Rev
// Zero-cost iterator adapters for structured traversal
// === Approach 1: step_by — every nth element ===
pub fn every_nth(data: &[i32], n: usize) -> Vec<i32> {
data.iter().step_by(n).copied().collect()
}
pub fn range_step(start: i32, stop: i32, step: usize) -> Vec<i32> {
(start..stop).step_by(step).collect()
}
// === Approach 2: enumerate — pair elements with their index ===
pub fn find_with_index(data: &[i32], pred: impl Fn(&i32) -> bool) -> Option<(usize, i32)> {
data.iter()
.enumerate()
.find(|(_, x)| pred(x))
.map(|(i, &x)| (i, x))
}
pub fn indexed_filter(data: &[i32], pred: impl Fn(&i32) -> bool) -> Vec<(usize, i32)> {
data.iter()
.enumerate()
.filter(|(_, x)| pred(x))
.map(|(i, &x)| (i, x))
.collect()
}
pub fn format_numbered(items: &[&str]) -> Vec<String> {
items
.iter()
.enumerate()
.map(|(i, s)| format!("{}. {}", i + 1, s))
.collect()
}
// === Approach 3: rev — reverse iteration ===
pub fn reverse_collect<T: Copy>(data: &[T]) -> Vec<T> {
data.iter().rev().copied().collect()
}
pub fn rev_map(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
data.iter().rev().map(|&x| f(x)).collect()
}
// Combining adapters: enumerate + rev to get (reversed-index, value)
pub fn enumerate_reversed(data: &[i32]) -> Vec<(usize, i32)> {
data.iter()
.rev()
.enumerate()
.map(|(i, &x)| (i, x))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
// --- step_by tests ---
#[test]
fn test_every_nth_step2() {
assert_eq!(every_nth(&[1, 2, 3, 4, 5, 6], 2), vec![1, 3, 5]);
}
#[test]
fn test_every_nth_step3() {
assert_eq!(
every_nth(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 3),
vec![0, 3, 6, 9]
);
}
#[test]
fn test_every_nth_step1() {
// step_by(1) returns all elements
assert_eq!(every_nth(&[10, 20, 30], 1), vec![10, 20, 30]);
}
#[test]
fn test_every_nth_empty() {
assert_eq!(every_nth(&[], 2), Vec::<i32>::new());
}
#[test]
fn test_range_step() {
assert_eq!(range_step(0, 10, 3), vec![0, 3, 6, 9]);
assert_eq!(range_step(1, 8, 2), vec![1, 3, 5, 7]);
}
// --- enumerate tests ---
#[test]
fn test_find_with_index_found() {
assert_eq!(
find_with_index(&[10, 20, 30, 40], |&x| x > 25),
Some((2, 30))
);
}
#[test]
fn test_find_with_index_not_found() {
assert_eq!(find_with_index(&[1, 2, 3], |&x| x > 100), None);
}
#[test]
fn test_indexed_filter() {
let result = indexed_filter(&[1, 2, 3, 4, 5], |&x| x % 2 == 0);
assert_eq!(result, vec![(1, 2), (3, 4)]);
}
#[test]
fn test_format_numbered() {
let items = vec!["alpha", "beta", "gamma"];
assert_eq!(
format_numbered(&items),
vec!["1. alpha", "2. beta", "3. gamma"]
);
}
// --- rev tests ---
#[test]
fn test_reverse_collect() {
assert_eq!(reverse_collect(&[1, 2, 3, 4, 5]), vec![5, 4, 3, 2, 1]);
}
#[test]
fn test_reverse_collect_empty() {
assert_eq!(reverse_collect::<i32>(&[]), Vec::<i32>::new());
}
#[test]
fn test_rev_map() {
// reverse then double
assert_eq!(rev_map(&[1, 2, 3], |x| x * 2), vec![6, 4, 2]);
}
#[test]
fn test_enumerate_reversed() {
// reversed enumeration: last element gets index 0
let result = enumerate_reversed(&[10, 20, 30]);
assert_eq!(result, vec![(0, 30), (1, 20), (2, 10)]);
}
// --- composition tests ---
#[test]
fn test_step_then_enumerate() {
let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let result: Vec<(usize, i32)> = data
.iter()
.step_by(2)
.enumerate()
.map(|(i, &x)| (i, x))
.collect();
assert_eq!(result, vec![(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]);
}
#[test]
fn test_step_by_rev() {
// every 2nd element, reversed
let data = [0, 1, 2, 3, 4, 5];
let result: Vec<i32> = data.iter().step_by(2).rev().copied().collect();
assert_eq!(result, vec![4, 2, 0]);
}
}#[cfg(test)]
mod tests {
use super::*;
// --- step_by tests ---
#[test]
fn test_every_nth_step2() {
assert_eq!(every_nth(&[1, 2, 3, 4, 5, 6], 2), vec![1, 3, 5]);
}
#[test]
fn test_every_nth_step3() {
assert_eq!(
every_nth(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 3),
vec![0, 3, 6, 9]
);
}
#[test]
fn test_every_nth_step1() {
// step_by(1) returns all elements
assert_eq!(every_nth(&[10, 20, 30], 1), vec![10, 20, 30]);
}
#[test]
fn test_every_nth_empty() {
assert_eq!(every_nth(&[], 2), Vec::<i32>::new());
}
#[test]
fn test_range_step() {
assert_eq!(range_step(0, 10, 3), vec![0, 3, 6, 9]);
assert_eq!(range_step(1, 8, 2), vec![1, 3, 5, 7]);
}
// --- enumerate tests ---
#[test]
fn test_find_with_index_found() {
assert_eq!(
find_with_index(&[10, 20, 30, 40], |&x| x > 25),
Some((2, 30))
);
}
#[test]
fn test_find_with_index_not_found() {
assert_eq!(find_with_index(&[1, 2, 3], |&x| x > 100), None);
}
#[test]
fn test_indexed_filter() {
let result = indexed_filter(&[1, 2, 3, 4, 5], |&x| x % 2 == 0);
assert_eq!(result, vec![(1, 2), (3, 4)]);
}
#[test]
fn test_format_numbered() {
let items = vec!["alpha", "beta", "gamma"];
assert_eq!(
format_numbered(&items),
vec!["1. alpha", "2. beta", "3. gamma"]
);
}
// --- rev tests ---
#[test]
fn test_reverse_collect() {
assert_eq!(reverse_collect(&[1, 2, 3, 4, 5]), vec![5, 4, 3, 2, 1]);
}
#[test]
fn test_reverse_collect_empty() {
assert_eq!(reverse_collect::<i32>(&[]), Vec::<i32>::new());
}
#[test]
fn test_rev_map() {
// reverse then double
assert_eq!(rev_map(&[1, 2, 3], |x| x * 2), vec![6, 4, 2]);
}
#[test]
fn test_enumerate_reversed() {
// reversed enumeration: last element gets index 0
let result = enumerate_reversed(&[10, 20, 30]);
assert_eq!(result, vec![(0, 30), (1, 20), (2, 10)]);
}
// --- composition tests ---
#[test]
fn test_step_then_enumerate() {
let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let result: Vec<(usize, i32)> = data
.iter()
.step_by(2)
.enumerate()
.map(|(i, &x)| (i, x))
.collect();
assert_eq!(result, vec![(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]);
}
#[test]
fn test_step_by_rev() {
// every 2nd element, reversed
let data = [0, 1, 2, 3, 4, 5];
let result: Vec<i32> = data.iter().step_by(2).rev().copied().collect();
assert_eq!(result, vec![4, 2, 0]);
}
}
Deep Comparison
OCaml vs Rust: Step By, Enumerate, Rev
Side-by-Side Code
OCaml
(* step_by: filter by index modulo *)
let step_by n lst = List.filteri (fun i _ -> i mod n = 0) lst
(* enumerate: pair with index *)
let enumerate lst = List.mapi (fun i x -> (i, x)) lst
(* rev_map: map over reversed list *)
let rev_map f lst = List.map f (List.rev lst)
Rust (idiomatic — zero-cost adapters)
fn every_nth(data: &[i32], n: usize) -> Vec<i32> {
data.iter().step_by(n).copied().collect()
}
fn indexed_filter(data: &[i32], pred: impl Fn(&i32) -> bool) -> Vec<(usize, i32)> {
data.iter().enumerate().filter(|(_, x)| pred(x)).map(|(i, &x)| (i, x)).collect()
}
fn rev_map(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
data.iter().rev().map(|&x| f(x)).collect()
}
Rust (functional/explicit — range_step recursive analog)
fn range_step(start: i32, stop: i32, step: usize) -> Vec<i32> {
(start..stop).step_by(step).collect()
}
fn enumerate_reversed(data: &[i32]) -> Vec<(usize, i32)> {
data.iter().rev().enumerate().map(|(i, &x)| (i, x)).collect()
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Step by n | val step_by : int -> 'a list -> 'a list | fn every_nth(data: &[i32], n: usize) -> Vec<i32> |
| Enumerate | val enumerate : 'a list -> (int * 'a) list | Iterator::enumerate() -> (usize, &T) |
| Reverse map | val rev_map : ('a -> 'b) -> 'a list -> 'b list | fn rev_map(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> |
| Index type | int (signed, 63-bit) | usize (unsigned, pointer-sized) |
| Collection type | 'a list (linked list) | &[T] (contiguous slice) |
Key Insights
List.filteri with i mod n = 0 visits every element; Rust's step_by internally skips n-1 elements between yields, making the intent explicit and the implementation potentially more efficient on slices.step_by, enumerate, and rev produce no allocation and do no work until .collect() or consumption. OCaml's list operations are strict and produce intermediate lists at each step.DoubleEndedIterator as a capability**: Rust's rev() requires the underlying iterator to implement DoubleEndedIterator. Slice iterators satisfy this because slices are contiguous memory with known endpoints. OCaml's singly-linked lists can only be traversed forwards; List.rev allocates a new list.step_by(2).rev() compiles only when the step iterator still implements DoubleEndedIterator. The compiler enforces composability. In OCaml, any two list functions compose freely but correctness is the programmer's responsibility.usize vs int**: Rust uses usize for indices — it cannot be negative, matching the reality that list positions are natural numbers. OCaml uses plain int, so negative indices are possible at the type level (and List.filteri would simply never match them).When to Use Each Style
**Use idiomatic Rust (.step_by(), .enumerate(), .rev())** when working with standard slices, ranges, or any iterator — these adapters are the zero-cost default and compose cleanly with the rest of the iterator API.
Use the recursive/explicit style when you need custom step logic tied to values rather than positions (e.g., "advance until condition"), or when implementing a non-standard traversal over a recursive data structure where the iterator protocol doesn't apply.
Exercises
diagonal_elements(matrix: &[Vec<T>]) -> Vec<&T> using .enumerate() to extract the main diagonal.interleave(a: &[i32], b: &[i32]) -> Vec<i32> using .step_by(2) and .chain() without explicit loops..enumerate().rev() to print a countdown with positions: "3: c", "2: b", "1: a" from input ["a", "b", "c"].