๐Ÿฆ€ Functional Rust

054: List Map from Scratch

Difficulty: 1 Level: Beginner Implement `map` from first principles โ€” and discover it's a special case of `fold`.

The Problem This Solves

You have a list of numbers and you want a list of their squares. A list of filenames and you want a list of file sizes. A list of users and you want a list of their emails. The operation is always the same: walk the list, apply a transformation to each element, collect the results. Without `map`, you write a `for` loop, a mutable accumulator, and the same boilerplate for every transformation. The loop structure obscures the intent. With `map`, you declare the transformation and `map` handles the iteration โ€” the loop is gone, only the logic remains. The deeper lesson: `add1`, `to_string`, and `double` all have the same shape. The Abstraction Principle says when two functions share a shape, factor out the difference into a parameter. `map` is that factoring โ€” it takes the transformation as a parameter.

The Intuition

`map` is a conveyor belt. Each item on the belt goes through a machine (your function `f`) and comes out the other side transformed. The belt is the same; only the machine changes. In OCaml, `map` recurses over a cons-list: empty list produces empty list, head gets transformed and prepended to the recursively mapped tail. In Rust, the iterator does the walking; you just describe the transformation.

How It Works in Rust

// Idiomatic: iterator adapter
pub fn map<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 list.iter().map(f).collect()
}

// Recursive: mirrors OCaml's head-cons pattern
pub fn map_recursive<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 match list {
     [] => vec![],
     [head, tail @ ..] => {  // [head, tail @ ..] โ‰ˆ OCaml's h :: t
         let mut result = vec![f(head)];
         result.extend(map_recursive(tail, f));
         result
     }
 }
}

// Fold: map as a fold over an accumulator
pub fn map_fold<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 list.iter().fold(Vec::new(), |mut acc, x| {
     acc.push(f(x));  // fold accumulates transformed elements
     acc
 })
}
All three produce the same result. The fold version reveals that `map` is mechanically a `fold` โ€” fold is the universal list combinator, and `map` is one of its specialisations.

What This Unlocks

Key Differences

ConceptOCamlRust
Type signature`('a -> 'b) -> 'a list -> 'b list``fn<A, B, F: Fn(&A)->B>(list: &[A], f: F) -> Vec<B>`
Head-tail pattern`h :: t``[head, tail @ ..]`
Partial application`let add1 = map (fun x -> x+1)`Explicit closure or wrapper function
AllocationGC manages cons cells`collect()` allocates one `Vec`
Iterator version`List.map` in stdlib`Iterator::map` (lazy, allocates on `collect`)
//! # List Map from Scratch
//! Deriving `List.map` from scratch โ€” the Abstraction Principle.
//!
//! Three implementations showing how `map` generalises element-by-element
//! list transformation: iterator adapter, explicit recursion, and fold.

/// Iterator-based map: apply `f` to every element and collect.
///
/// This is idiomatic Rust โ€” the standard library's `Iterator::map` does
/// exactly this under the hood, but we call `.collect()` explicitly to
/// mirror the OCaml `'a list -> 'b list` shape.
pub fn map<A, B, F>(list: &[A], f: F) -> Vec<B>
where
    F: Fn(&A) -> B,
{
    list.iter().map(f).collect()
}

/// Recursive map: head-cons pattern mirroring the OCaml definition.
///
/// ```ocaml
/// let rec map f = function
///   | [] -> []
///   | h :: t -> let h' = f h in h' :: map f t
/// ```
pub fn map_recursive<A, B, F>(list: &[A], f: F) -> Vec<B>
where
    F: Fn(&A) -> B,
{
    match list {
        [] => vec![],
        [head, tail @ ..] => {
            let mut result = vec![f(head)];
            result.extend(map_recursive(tail, f));
            result
        }
    }
}

/// Fold-based map: build the result left-to-right with `fold`.
///
/// Demonstrates that `map` is a special case of `fold` where each step
/// appends a transformed element.
pub fn map_fold<A, B, F>(list: &[A], f: F) -> Vec<B>
where
    F: Fn(&A) -> B,
{
    list.iter().fold(Vec::new(), |mut acc, x| {
        acc.push(f(x));
        acc
    })
}

// Partially-applied helpers matching the OCaml `add1`, `to_string`, `double`.

pub fn add1(list: &[i32]) -> Vec<i32> {
    map(list, |x| x + 1)
}

pub fn to_string(list: &[i32]) -> Vec<String> {
    map(list, |x| x.to_string())
}

pub fn double(list: &[i32]) -> Vec<i32> {
    map(list, |x| x * 2)
}

#[cfg(test)]
mod tests {
    use super::*;

    const NUMS: &[i32] = &[1, 2, 3, 4, 5];

    // --- iterator map ---

    #[test]
    fn map_add1() {
        assert_eq!(map(NUMS, |x| x + 1), vec![2, 3, 4, 5, 6]);
    }

    #[test]
    fn map_to_string() {
        assert_eq!(map(NUMS, |x| x.to_string()), vec!["1", "2", "3", "4", "5"]);
    }

    #[test]
    fn map_double() {
        assert_eq!(map(NUMS, |x| x * 2), vec![2, 4, 6, 8, 10]);
    }

    #[test]
    fn map_empty() {
        assert_eq!(map::<i32, i32, _>(&[], |x| x + 1), vec![]);
    }

    // --- recursive map ---

    #[test]
    fn map_recursive_add1() {
        assert_eq!(map_recursive(NUMS, |x| x + 1), vec![2, 3, 4, 5, 6]);
    }

    #[test]
    fn map_recursive_double() {
        assert_eq!(map_recursive(NUMS, |x| x * 2), vec![2, 4, 6, 8, 10]);
    }

    #[test]
    fn map_recursive_empty() {
        assert_eq!(map_recursive::<i32, i32, _>(&[], |x| x + 1), vec![]);
    }

    // --- fold map ---

    #[test]
    fn map_fold_add1() {
        assert_eq!(map_fold(NUMS, |x| x + 1), vec![2, 3, 4, 5, 6]);
    }

    #[test]
    fn map_fold_double() {
        assert_eq!(map_fold(NUMS, |x| x * 2), vec![2, 4, 6, 8, 10]);
    }

    #[test]
    fn map_fold_empty() {
        assert_eq!(map_fold::<i32, i32, _>(&[], |x| x + 1), vec![]);
    }

    // --- helpers ---

    #[test]
    fn helpers_match_ocaml_output() {
        assert_eq!(add1(NUMS), vec![2, 3, 4, 5, 6]);
        assert_eq!(to_string(NUMS), vec!["1", "2", "3", "4", "5"]);
        assert_eq!(double(NUMS), vec![2, 4, 6, 8, 10]);
    }

    // All three implementations must agree
    #[test]
    fn all_implementations_agree() {
        let f = |x: &i32| x * x;
        assert_eq!(map(NUMS, f), map_recursive(NUMS, f));
        assert_eq!(map(NUMS, f), map_fold(NUMS, f));
    }
}

fn main() {
    println!("{:?}", map(NUMS, |x| x + 1), vec![2, 3, 4, 5, 6]);
    println!("{:?}", map(NUMS, |x| x.to_string()), vec!["1", "2", "3", "4", "5"]);
    println!("{:?}", map(NUMS, |x| x * 2), vec![2, 4, 6, 8, 10]);
}
let rec map f = function
  | [] -> []
  | h :: t -> let h' = f h in h' :: map f t

let add1 = map (fun x -> x + 1)
let to_string = map string_of_int
let double = map (fun x -> x * 2)

let () =
  let nums = [1; 2; 3; 4; 5] in
  List.iter (Printf.printf "%d ") (add1 nums); print_newline ();
  List.iter (Printf.printf "%s ") (to_string nums); print_newline ();
  List.iter (Printf.printf "%d ") (double nums); print_newline ()

๐Ÿ“Š Detailed Comparison

Comparison: List Map from Scratch

OCaml โ€” recursive definition

๐Ÿช Show OCaml equivalent
let rec map f = function
| [] -> []
| h :: t -> let h' = f h in h' :: map f t

Rust โ€” iterator (idiomatic)

pub fn map<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 list.iter().map(f).collect()
}

Rust โ€” recursive (mirrors OCaml)

pub fn map_recursive<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 match list {
     [] => vec![],
     [head, tail @ ..] => {
         let mut result = vec![f(head)];
         result.extend(map_recursive(tail, f));
         result
     }
 }
}

Rust โ€” fold

pub fn map_fold<A, B, F: Fn(&A) -> B>(list: &[A], f: F) -> Vec<B> {
 list.iter().fold(Vec::new(), |mut acc, x| {
     acc.push(f(x));
     acc
 })
}

Partial application comparison

OCamlRust
`let add1 = map (fun x -> x + 1)``fn add1(list: &[i32]) -> Vec<i32> { map(list, \x\x + 1) }`
`let double = map (fun x -> x 2)``fn double(list: &[i32]) -> Vec<i32> { map(list, \x\x 2) }`
`let to_string = map string_of_int``fn to_string(list: &[i32]) -> Vec<String> { map(list, \x\x.to_string()) }`
OCaml curries automatically; Rust requires explicit wrapping because `map` takes two arguments rather than returning a closure.

Comparison Table

AspectOCamlRust
Type signature`('a -> 'b) -> 'a list -> 'b list``fn<A,B,F:Fn(&A)->B>(&[A], F) -> Vec<B>`
Pattern match`\[] -> [] \h :: t -> ...``[] => vec![] \[head, tail @ ..] => ...`
Partial applicationNative curryingClosure wrapping
Memory modelGC-managed cons cellsContiguous `Vec<B>` on heap
Tail recursionNot tail-recursive (OCaml TCO not applied here)Not tail-recursive (use iterator version)
Idiomatic styleRecursive with pattern match`iter().map().collect()`

Takeaways

  • `map` is the canonical abstraction for element-wise list transformation
  • Rust's iterator `map` adapter and OCaml's `List.map` have the same semantics; both are lazy/eager respectively
  • The recursive Rust version uses slice patterns (`[head, tail @ ..]`) which are the closest structural analogue to OCaml's cons patterns
  • Fold proves `map` is a specialisation: every `map` can be rewritten as a `fold`