1000 — List Map
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
xs.iter().map(f).collect() as the idiomatic Rust map operation&T from T in iterator closures: map(|x| f(x)) where x: &TIterator::map to OCaml's List.map f lstmap is the functor operation: pure transformation without side effectsCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Idiomatic | .iter().map(f).collect() | List.map f lst |
| Element type | &T from .iter() | T (value) |
| Tail recursion | Iterative (iterator) | List.rev_map + List.rev |
| Lazy | Yes (iterator chain) | No (List.map is eager) |
| Type inference | Both T and U inferred | Fully inferred |
| Ownership | .iter() borrows; .into_iter() consumes | Value 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]);
}
}#[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:
& borrowing to avoid consuming numbersRecursive Implementation
OCaml: Naive Recursion
let rec map f = function
| [] -> []
| x :: xs -> f x :: map f xs
Pros:
Cons:
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:
Cons:
List.rev at the endRust: 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:
Cons:
xs.remove(0) is O(n) per element → O(n²) totalRust: 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:
Cons:
Type Signatures Comparison
| Aspect | OCaml | Rust |
|---|---|---|
| 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 -> 'b | Fn(&T) -> U (trait) |
| Collection Type | list (persistent) | Vec<T> (mutable heap) / &[T] (slice) |
| Closure | fun x -> ... | \|x\| ... |
| Reference | Implicit | Explicit (&T) |
| Lifetime | None | '_ (implicit) |
OCaml Type Example:
List.map : ('a -> 'b) -> 'a list -> 'b list
Rust Type Example:
fn map_iter<T, U, F>(xs: &[T], f: F) -> Vec<U>
where F: Fn(&T) -> U
&[T])Fn traitImplementation Characteristics
Memory Allocation
OCaml
(* Creates intermediate list *)
let doubled = List.map (fun x -> x * 2) numbers
Rust (Iterator)
let doubled = map_iter(&numbers, |x| x * 2);
Vec::collect()Rust (Recursive)
let doubled = map_recursive(numbers, |x| x * 2);
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:
.filter().map().fold())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 listRust's explicit borrowing requires thought:
map_iter(&xs, f) borrows but doesn't consumemap_recursive(xs, f) consumes ownershipLesson: Borrows are cheaper than moves for map operations.
4. Lazy vs. Eager Evaluation
| OCaml | Rust |
|---|---|
List.map eagerly evaluates | iter().map() lazily evaluates |
| All transformations happen immediately | Transformations happen on .collect() |
| Can't express infinite sequences | Can express infinite lazily-evaluated sequences |
| Simpler mental model | More 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:
Performance Comparison
Allocation Count (for 5-element list)
| Approach | OCaml | Rust |
|---|---|---|
List.map | 5 node allocations | 1 Vec allocation |
Single .map() | 5 allocations | 1 allocation |
Two chained .map() | 10 allocations | 1 allocation |
| Recursive naive | 5 allocations | N/A (not recommended) |
| Recursive tail | 5 allocations | 1 allocation (but with removes) |
Winner: Rust iterators by far (O(1) allocations for chains)
Time Complexity (for n-element list)
| Approach | OCaml | Rust |
|---|---|---|
List.map | O(n) | O(n) |
Single .map() | O(n) | O(n) |
| Recursive naive | O(n) stack frames | Stack overflow risk |
| Recursive tail | O(n) optimized | O(n²) with remove |
| Chained iterators | O(n) × chain length | O(n) (fused) |
Winner: Rust iterators (fused operations), OCaml tail-recursive as backup
Test Coverage
OCaml Tests (11 assertions)
Rust Tests (15 assertions)
Conversion Checklist: OCaml → Rust
When converting OCaml List.map patterns to Rust:
List.map f xs with xs.iter().map(f).collect()[a; b; c] to vec![a, b, c] or &[a, b, c]&T vs. owned T.filter(), .map(), .fold() for complex transformations.collect::<Vec<_>>() to force evaluationSummary
| Dimension | OCaml | Rust |
|---|---|---|
| Syntax | Concise, inferred | Explicit, verbose |
| Performance | Eager, allocates intermediates | Lazy, fused operations |
| Idiomatic | Recursion + pattern matching | Iterators + trait bounds |
| Safety | Garbage collected | Ownership + borrowing |
| Type checking | Implicit polymorphism | Explicit generics |
| Parallel | Requires threads/async | Built-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
Vec<String> to produce a Vec<usize> of string lengths using .map(|s| s.len()).map_with_index<T, U>(xs: &[T], f: impl Fn(usize, &T) -> U) -> Vec<U> using .enumerate().map_option<T, U>(xs: &[Option<T>], f: impl Fn(&T) -> U) -> Vec<Option<U>>.map with filter: keep only elements whose mapped value is Some.map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list (equivalent to List.map2 from the standard library).