ExamplesBy LevelBy TopicLearning Paths
1000 Intermediate

1000 — List Map

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "1000 — List Map" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Apply a function to every element of a list and collect the results. Key difference from OCaml: | Aspect | Rust | OCaml |

Tutorial

The Problem

Apply a function to every element of a list and collect the results. Implement both an iterator-based map_iter (idiomatic Rust) and a recursive map_recursive (functional style mirroring OCaml). Compare with OCaml's List.map and a custom tail-recursive implementation.

🎯 Learning Outcomes

  • • Use xs.iter().map(f).collect() as the idiomatic Rust map operation
  • • Understand that the iterator chain is lazy — no values are computed until collected
  • • Implement a tail-recursive accumulator version: build result in reverse, then reverse at end
  • • Distinguish &T from T in iterator closures: map(|x| f(x)) where x: &T
  • • Map Rust's Iterator::map to OCaml's List.map f lst
  • • Recognise that map is the functor operation: pure transformation without side effects
  • Code Example

    let numbers = vec![1, 2, 3, 4, 5];
    let doubled = map_iter(&numbers, |x| x * 2);
    println!("{:?}", doubled);
    // Output: [2, 4, 6, 8, 10]

    Key Differences

    AspectRustOCaml
    Idiomatic.iter().map(f).collect()List.map f lst
    Element type&T from .iter()T (value)
    Tail recursionIterative (iterator)List.rev_map + List.rev
    LazyYes (iterator chain)No (List.map is eager)
    Type inferenceBoth T and U inferredFully inferred
    Ownership.iter() borrows; .into_iter() consumesValue semantics

    map is the foundational list transformation. In Rust, it integrates into the iterator chain allowing further adapters before collecting. In OCaml, it returns a new list immediately. Both represent the same mathematical functor: applying a morphism f: A -> B to all elements.

    OCaml Approach

    List.map (fun x -> x * 2) numbers is the standard call. The custom let rec map_recursive f = function | [] -> [] | x :: xs -> f x :: map_recursive f xs mirrors the Rust recursive version. OCaml's List.map is not tail-recursive for large lists (use List.rev_map + List.rev for tail-recursive mapping). The iterator version in Rust avoids this issue entirely.

    Full Source

    #![allow(clippy::all)]
    //! List mapping examples: idiomatic Rust iterators and functional recursion.
    //!
    //! This module demonstrates two approaches to applying a function to each element
    //! of a list:
    //! - **Idiomatic Rust**: Using iterators with `.iter().map().collect()`
    //! - **Functional/Recursive**: Tail-recursive style, similar to OCaml's List.map
    
    /// Apply a function to each element using iterators (idiomatic Rust).
    ///
    /// This is the preferred approach in Rust. It uses iterator adapters which are
    /// lazy evaluated, composable, and optimized by the compiler.
    ///
    /// # Arguments
    /// * `xs` - A slice of elements
    /// * `f` - A function to apply to each element
    ///
    /// # Example
    /// ```
    /// use example_1000_list_map::map_iter;
    /// let numbers = vec![1, 2, 3, 4, 5];
    /// let doubled = map_iter(&numbers, |x| x * 2);
    /// assert_eq!(doubled, vec![2, 4, 6, 8, 10]);
    /// ```
    pub fn map_iter<T, U, F>(xs: &[T], f: F) -> Vec<U>
    where
        F: Fn(&T) -> U,
    {
        xs.iter().map(f).collect()
    }
    
    /// Apply a function to each element using recursion (functional style).
    ///
    /// This approach mirrors OCaml's List.map more closely, using tail recursion.
    /// In Rust, this is less idiomatic but demonstrates functional programming patterns.
    ///
    /// # Arguments
    /// * `xs` - A vector of elements (owned, consumed)
    /// * `f` - A function to apply to each element
    ///
    /// # Example
    /// ```
    /// use example_1000_list_map::map_recursive;
    /// let numbers = vec![1, 2, 3, 4, 5];
    /// let doubled = map_recursive(numbers.clone(), |x| x * 2);
    /// assert_eq!(doubled, vec![2, 4, 6, 8, 10]);
    /// ```
    pub fn map_recursive<T, U, F>(xs: Vec<T>, f: F) -> Vec<U>
    where
        F: Fn(T) -> U,
    {
        fn go<T, U, F>(mut xs: Vec<T>, f: &F, mut acc: Vec<U>) -> Vec<U>
        where
            F: Fn(T) -> U,
        {
            if xs.is_empty() {
                acc
            } else {
                let head = xs.remove(0);
                acc.push(f(head));
                go(xs, f, acc)
            }
        }
    
        go(xs, &f, Vec::new())
    }
    
    /// Alternative recursive implementation using pattern matching on the vector directly.
    /// This version is more elegant but requires consuming the vector via conversion.
    pub fn map_recursive_match<T, U, F>(mut xs: Vec<T>, f: &F) -> Vec<U>
    where
        F: Fn(T) -> U,
    {
        if xs.is_empty() {
            Vec::new()
        } else {
            let head = xs.remove(0);
            let mut tail_result = map_recursive_match(xs, f);
            let mut result = vec![f(head)];
            result.append(&mut tail_result);
            result
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_map_iter_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_iter(&empty, |x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_iter_single() {
            let single = vec![5];
            let result = map_iter(&single, |x| x * 2);
            assert_eq!(result, vec![10]);
        }
    
        #[test]
        fn test_map_iter_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_iter(&numbers, |x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_iter_negative() {
            let numbers: Vec<i32> = vec![-1, -2, -3];
            let result = map_iter(&numbers, |&x| x.abs());
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_map_iter_type_conversion() {
            let numbers = vec![1, 2, 3];
            let result = map_iter(&numbers, |x| x.to_string());
            assert_eq!(result, vec!["1", "2", "3"]);
        }
    
        #[test]
        fn test_map_recursive_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_recursive(empty, |x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_recursive_single() {
            let single = vec![5];
            let result = map_recursive(single, |x| x * 2);
            assert_eq!(result, vec![10]);
        }
    
        #[test]
        fn test_map_recursive_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive(numbers, |x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_recursive_negative() {
            let numbers = vec![-1 as i32, -2 as i32, -3 as i32];
            let result = map_recursive(numbers, |x: i32| x.abs());
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_map_recursive_match_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_recursive_match(empty, &|x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_recursive_match_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive_match(numbers, &|x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_iter_squares() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_iter(&numbers, |x| x * x);
            assert_eq!(result, vec![1, 4, 9, 16, 25]);
        }
    
        #[test]
        fn test_map_recursive_squares() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive(numbers, |x| x * x);
            assert_eq!(result, vec![1, 4, 9, 16, 25]);
        }
    
        #[test]
        fn test_map_iter_with_captures() {
            let numbers = vec![1, 2, 3];
            let multiplier = 3;
            let result = map_iter(&numbers, |x| x * multiplier);
            assert_eq!(result, vec![3, 6, 9]);
        }
    
        #[test]
        fn test_map_recursive_with_captures() {
            let numbers = vec![1, 2, 3];
            let multiplier = 3;
            let result = map_recursive(numbers, |x| x * multiplier);
            assert_eq!(result, vec![3, 6, 9]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_map_iter_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_iter(&empty, |x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_iter_single() {
            let single = vec![5];
            let result = map_iter(&single, |x| x * 2);
            assert_eq!(result, vec![10]);
        }
    
        #[test]
        fn test_map_iter_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_iter(&numbers, |x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_iter_negative() {
            let numbers: Vec<i32> = vec![-1, -2, -3];
            let result = map_iter(&numbers, |&x| x.abs());
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_map_iter_type_conversion() {
            let numbers = vec![1, 2, 3];
            let result = map_iter(&numbers, |x| x.to_string());
            assert_eq!(result, vec!["1", "2", "3"]);
        }
    
        #[test]
        fn test_map_recursive_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_recursive(empty, |x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_recursive_single() {
            let single = vec![5];
            let result = map_recursive(single, |x| x * 2);
            assert_eq!(result, vec![10]);
        }
    
        #[test]
        fn test_map_recursive_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive(numbers, |x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_recursive_negative() {
            let numbers = vec![-1 as i32, -2 as i32, -3 as i32];
            let result = map_recursive(numbers, |x: i32| x.abs());
            assert_eq!(result, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_map_recursive_match_empty() {
            let empty: Vec<i32> = vec![];
            let result = map_recursive_match(empty, &|x| x * 2);
            assert_eq!(result, vec![]);
        }
    
        #[test]
        fn test_map_recursive_match_multiple() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive_match(numbers, &|x| x * 2);
            assert_eq!(result, vec![2, 4, 6, 8, 10]);
        }
    
        #[test]
        fn test_map_iter_squares() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_iter(&numbers, |x| x * x);
            assert_eq!(result, vec![1, 4, 9, 16, 25]);
        }
    
        #[test]
        fn test_map_recursive_squares() {
            let numbers = vec![1, 2, 3, 4, 5];
            let result = map_recursive(numbers, |x| x * x);
            assert_eq!(result, vec![1, 4, 9, 16, 25]);
        }
    
        #[test]
        fn test_map_iter_with_captures() {
            let numbers = vec![1, 2, 3];
            let multiplier = 3;
            let result = map_iter(&numbers, |x| x * multiplier);
            assert_eq!(result, vec![3, 6, 9]);
        }
    
        #[test]
        fn test_map_recursive_with_captures() {
            let numbers = vec![1, 2, 3];
            let multiplier = 3;
            let result = map_recursive(numbers, |x| x * multiplier);
            assert_eq!(result, vec![3, 6, 9]);
        }
    }

    Deep Comparison

    OCaml ↔ Rust: List Map Comparison

    Side-by-Side Code

    Basic Implementation

    OCaml: List.map (Idiomatic)
    let numbers = [1; 2; 3; 4; 5]
    let doubled = List.map (fun x -> x * 2) numbers
    let () = List.iter (fun x -> Printf.printf "%d " x) doubled
    (* Output: 2 4 6 8 10 *)
    
    Rust: Iterator (Idiomatic)
    let numbers = vec![1, 2, 3, 4, 5];
    let doubled = map_iter(&numbers, |x| x * 2);
    println!("{:?}", doubled);
    // Output: [2, 4, 6, 8, 10]
    

    Observations:

  • • OCaml syntax is more compact (fewer brackets and type hints)
  • • Rust requires explicit & borrowing to avoid consuming numbers
  • • Both are one-liners for the mapping operation

  • Recursive Implementation

    OCaml: Naive Recursion
    let rec map f = function
      | [] -> []
      | x :: xs -> f x :: map f xs
    

    Pros:

  • • Very concise (3 lines)
  • • Directly follows list structure
  • • Pattern matching is natural
  • Cons:

  • • Not tail-recursive (can stack overflow on large lists)
  • • Creates intermediate list nodes

  • OCaml: Tail-Recursive (Optimized)
    let map_tail f xs =
      let rec go acc = function
        | [] -> List.rev acc
        | x :: xs -> go (f x :: acc) xs
      in
      go [] xs
    

    Pros:

  • • Tail-call optimized (O(1) stack space)
  • • Linear time complexity
  • • No risk of stack overflow
  • Cons:

  • • More complex code
  • • Requires List.rev at the end

  • Rust: Recursive (Functional Style)
    pub fn map_recursive<T, U, F>(xs: Vec<T>, f: F) -> Vec<U>
    where
        F: Fn(T) -> U,
    {
        fn go<T, U, F>(mut xs: Vec<T>, f: &F, mut acc: Vec<U>) -> Vec<U>
        where
            F: Fn(T) -> U,
        {
            if xs.is_empty() {
                acc
            } else {
                let head = xs.remove(0);
                acc.push(f(head));
                go(xs, f, acc)
            }
        }
    
        go(xs, &f, Vec::new())
    }
    

    Pros:

  • • Demonstrates functional style in Rust
  • • Conceptually similar to OCaml
  • Cons:

  • • Not idiomatic Rust
  • xs.remove(0) is O(n) per element → O(n²) total
  • • No tail-call optimization (Rust doesn't guarantee it)
  • • More verbose type annotations

  • Rust: Idiomatic (Iterator)
    pub fn map_iter<T, U, F>(xs: &[T], f: F) -> Vec<U>
    where
        F: Fn(&T) -> U,
    {
        xs.iter().map(f).collect()
    }
    

    Pros:

  • • Concise and idiomatic
  • • Zero-cost abstraction
  • • Lazy evaluation
  • • Composable with other iterator methods
  • • Compiler can optimize aggressively
  • Cons:

  • • Less obviously "functional" to beginners
  • • Closure captures by reference

  • Type Signatures Comparison

    AspectOCamlRust
    Signature('a -> 'b) -> 'a list -> 'b list<T, U, F>(xs: &[T], f: F) -> Vec<U> where F: Fn(&T) -> U
    Type Variables'a, 'b (implicit)T, U, F (explicit)
    Function Type'a -> 'bFn(&T) -> U (trait)
    Collection Typelist (persistent)Vec<T> (mutable heap) / &[T] (slice)
    Closurefun x -> ...\|x\| ...
    ReferenceImplicitExplicit (&T)
    LifetimeNone'_ (implicit)

    OCaml Type Example:

    List.map : ('a -> 'b) -> 'a list -> 'b list
    
  • • Universally quantified polymorphism
  • • Implicit reference handling
  • • Function is a first-class value type
  • Rust Type Example:

    fn map_iter<T, U, F>(xs: &[T], f: F) -> Vec<U>
    where F: Fn(&T) -> U
    
  • • Generic with trait bounds
  • • Explicit borrowing (&[T])
  • • Function must satisfy Fn trait

  • Implementation Characteristics

    Memory Allocation

    OCaml
    (* Creates intermediate list *)
    let doubled = List.map (fun x -> x * 2) numbers
    
  • Cost: O(n) heap allocations (one per list node)
  • Space: O(n) for result + O(n) for stack frames
  • Cleanup: Automatic garbage collection
  • Rust (Iterator)
    let doubled = map_iter(&numbers, |x| x * 2);
    
  • Cost: One allocation for final Vec::collect()
  • Space: O(n) for result
  • Cleanup: RAII (automatic on drop)
  • Rust (Recursive)
    let doubled = map_recursive(numbers, |x| x * 2);
    
  • Cost: One allocation (the accumulator Vec) + n remove calls
  • Space: O(n) for result + O(n) for recursion stack
  • Cleanup: RAII (automatic on drop)

  • Five Key Insights

    1. Iterators > Recursion in Rust

    Rust's iterator abstraction is fundamentally different from OCaml's pattern matching. While OCaml naturally expresses recursion through pattern matching on list structure, Rust's iterators are:

  • • More efficient (no intermediate vectors)
  • • More composable (.filter().map().fold())
  • • More idiomatic (what Rust developers expect)
  • • Better optimized (compiler can vectorize)
  • Lesson: Don't write OCaml-style recursive functions in Rust. Use iterators.

    2. Ownership Prevents Hidden Costs

    (* This creates TWO lists silently *)
    let doubled = List.map (fun x -> x * 2) numbers
    let tripled = List.map (fun x -> x * 3) doubled
    
    // This creates only ONE list (chained iterators)
    let result: Vec<i32> = numbers.iter()
        .map(|x| x * 2)
        .map(|x| x * 3)
        .collect();  // Only one allocation here!
    

    Rust's ownership model makes allocation explicit. You can't accidentally create intermediate structures.

    3. Reference Semantics Matter

    OCaml's implicit reference handling means:

  • List.map always returns a new list
  • • Original list is never modified
  • • References are transparent
  • Rust's explicit borrowing requires thought:

  • map_iter(&xs, f) borrows but doesn't consume
  • map_recursive(xs, f) consumes ownership
  • • Reference type affects performance
  • Lesson: Borrows are cheaper than moves for map operations.

    4. Lazy vs. Eager Evaluation

    OCamlRust
    List.map eagerly evaluatesiter().map() lazily evaluates
    All transformations happen immediatelyTransformations happen on .collect()
    Can't express infinite sequencesCan express infinite lazily-evaluated sequences
    Simpler mental modelMore efficient for chains
    (* Both transformations executed immediately *)
    List.map (fun x -> x * 2) 
      (List.map (fun x -> x + 1) numbers)
    
    // Only executed when `.collect()` is called
    numbers.iter()
        .map(|x| x + 1)
        .map(|x| x * 2)
        .collect::<Vec<_>>()
    

    5. Closure Capture Differences

    OCaml closures automatically capture variables:

    let multiplier = 3
    let triple = List.map (fun x -> x * multiplier) numbers
    (* multiplier is captured *)
    

    Rust closures can capture by reference or move:

    let multiplier = 3;
    let result = map_iter(&numbers, |x| x * multiplier);  // Captures &multiplier
    

    This affects:

  • • Memory lifetime
  • • Mutable vs. immutable captures
  • • Thread safety

  • Performance Comparison

    Allocation Count (for 5-element list)

    ApproachOCamlRust
    List.map5 node allocations1 Vec allocation
    Single .map()5 allocations1 allocation
    Two chained .map()10 allocations1 allocation
    Recursive naive5 allocationsN/A (not recommended)
    Recursive tail5 allocations1 allocation (but with removes)

    Winner: Rust iterators by far (O(1) allocations for chains)

    Time Complexity (for n-element list)

    ApproachOCamlRust
    List.mapO(n)O(n)
    Single .map()O(n)O(n)
    Recursive naiveO(n) stack framesStack overflow risk
    Recursive tailO(n) optimizedO(n²) with remove
    Chained iteratorsO(n) × chain lengthO(n) (fused)

    Winner: Rust iterators (fused operations), OCaml tail-recursive as backup


    Test Coverage

    OCaml Tests (11 assertions)

  • • Empty list
  • • Single element
  • • Multiple elements
  • • Type conversion (int → string)
  • • Negative numbers (abs)
  • • Tail-recursive equivalence
  • • Chained transformations
  • Rust Tests (15 assertions)

  • • Empty vector
  • • Single element
  • • Multiple elements
  • • Type conversion (i32 → String)
  • • Negative numbers
  • • Squaring (higher-order operations)
  • • Closure captures
  • • All three implementations tested

  • Conversion Checklist: OCaml → Rust

    When converting OCaml List.map patterns to Rust:

  • • [ ] Replace List.map f xs with xs.iter().map(f).collect()
  • • [ ] Change [a; b; c] to vec![a, b, c] or &[a, b, c]
  • • [ ] Replace pattern matching with iterator methods
  • • [ ] Add type annotations (Rust may not infer them)
  • • [ ] Consider lifetime implications for &T vs. owned T
  • • [ ] Use .filter(), .map(), .fold() for complex transformations
  • • [ ] Avoid recursion; use iterators instead
  • • [ ] Test with .collect::<Vec<_>>() to force evaluation

  • Summary

    DimensionOCamlRust
    SyntaxConcise, inferredExplicit, verbose
    PerformanceEager, allocates intermediatesLazy, fused operations
    IdiomaticRecursion + pattern matchingIterators + trait bounds
    SafetyGarbage collectedOwnership + borrowing
    Type checkingImplicit polymorphismExplicit generics
    ParallelRequires threads/asyncBuilt-in (fearless concurrency)

    Both are excellent functional languages. OCaml excels at pattern matching and quick prototyping. Rust excels at performance, memory safety, and large-scale systems. Choose the tool for the problem.

    Exercises

  • Map over a Vec<String> to produce a Vec<usize> of string lengths using .map(|s| s.len()).
  • Implement map_with_index<T, U>(xs: &[T], f: impl Fn(usize, &T) -> U) -> Vec<U> using .enumerate().
  • Write map_option<T, U>(xs: &[Option<T>], f: impl Fn(&T) -> U) -> Vec<Option<U>>.
  • Chain map with filter: keep only elements whose mapped value is Some.
  • In OCaml, implement map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list (equivalent to List.map2 from the standard library).
  • Open Source Repos