ExamplesBy LevelBy TopicLearning Paths
894 Intermediate

894-step-by — Step By, Enumerate, Rev

Functional Programming

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

  • • Use .step_by(n) to visit every nth element of an iterator
  • • Use .enumerate() to pair elements with zero-based indices
  • • Use .rev() to iterate in reverse over a DoubleEndedIterator
  • • Compose step_by, enumerate, and rev to produce complex traversal patterns
  • • Compare with OCaml's List.filteri, List.mapi, and List.rev
  • Code 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

  • Zero-cost composition: Rust adapters compose without intermediate allocation; OCaml List.filteri followed by List.mapi allocates two intermediate lists.
  • step_by on ranges: Rust (0..100).step_by(3) is a built-in lazy range; OCaml requires explicit List.init with stride arithmetic.
  • 1-based indexing: Rust enumeration is 0-based; OCaml mapi is also 0-based. Both require +1 for 1-based display.
  • rev composition: Rust .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]);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Step by nval step_by : int -> 'a list -> 'a listfn every_nth(data: &[i32], n: usize) -> Vec<i32>
    Enumerateval enumerate : 'a list -> (int * 'a) listIterator::enumerate() -> (usize, &T)
    Reverse mapval rev_map : ('a -> 'b) -> 'a list -> 'b listfn rev_map(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32>
    Index typeint (signed, 63-bit)usize (unsigned, pointer-sized)
    Collection type'a list (linked list)&[T] (contiguous slice)

    Key Insights

  • Zero-cost vs linear scan: OCaml's 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.
  • Lazy vs eager evaluation: Rust's iterator adapters are lazy — 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.
  • Composition is type-safe: 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

  • Write diagonal_elements(matrix: &[Vec<T>]) -> Vec<&T> using .enumerate() to extract the main diagonal.
  • Implement interleave(a: &[i32], b: &[i32]) -> Vec<i32> using .step_by(2) and .chain() without explicit loops.
  • Use .enumerate().rev() to print a countdown with positions: "3: c", "2: b", "1: a" from input ["a", "b", "c"].
  • Open Source Repos