๐Ÿฆ€ Functional Rust

006: Function Composition

Difficulty: 2 Level: Intermediate Build complex data transformations by snapping simple functions together โ€” like LEGO blocks for logic.

The Problem This Solves

Imagine you're cleaning user-submitted text: trim whitespace, lowercase it, strip punctuation, collapse double spaces. Without composition, you either write one huge function that does everything (hard to test, impossible to reuse), or you chain temporary variables one after another (messy, verbose). In many languages you'd write helper utilities and call them in sequence: `strip(lower(trim(input)))`. But reading inside-out is awkward. Some languages have a pipe operator (`|>`) to flip this into left-to-right reading. Rust doesn't have a built-in pipe, but it doesn't need one. Rust's iterator method chaining is composition. When you write `.map(f).filter(g).map(h)`, you're composing three functions into a pipeline that reads left to right, runs lazily, and produces zero intermediate allocations. This is both more readable and faster than equivalent imperative loops.

The Intuition

In Python you might write:
result = [x * 2 for x in data if x * 2 % 2 == 0]
Or in JavaScript:
data.map(x => x * 2).filter(x => x % 2 === 0).reduce((a, b) => a + b, 0)
Rust's iterator chains look almost identical to that JS version โ€” except they're lazy (nothing runs until you ask for a result) and the compiler optimizes the whole chain into a single loop. For composing standalone functions, Rust uses closures. The `compose(f, g)` pattern โ€” where you get back a new function that applies `g` then `f` โ€” is just a function that returns a closure. The `move` keyword in the closure captures `f` and `g` by value so they live long enough.

How It Works in Rust

// Compose two functions: g runs first, then f
pub fn compose<A, B, C>(
 f: impl Fn(B) -> C + 'static,
 g: impl Fn(A) -> B + 'static,
) -> Box<dyn Fn(A) -> C> {
 Box::new(move |x| f(g(x)))  // move captures f and g into the closure
}

// Usage: double_then_inc(5) โ†’ 5*2=10, then +1 = 11
let double = |x: i64| x * 2;
let inc    = |x: i64| x + 1;
let double_then_inc = compose(inc, double);
assert_eq!(double_then_inc(5), 11);

// Idiomatic Rust: iterator method chain IS composition
pub fn process(data: &[i64]) -> i64 {
 data.iter()
     .map(|x| x * 2)       // step 1: double each
     .filter(|x| x % 2 == 0) // step 2: keep evens (all of them, since we just doubled)
     .sum()                 // step 3: add up
}

// String pipeline: method chaining composes string transforms
pub fn process_string(s: &str) -> String {
 s.trim()
     .to_lowercase()
     .replace("  ", " ")
     .chars()
     .filter(|c| c.is_alphanumeric() || *c == ' ')
     .collect()  // collect() is what triggers the whole chain to run
}
The `Box<dyn Fn(A) -> C>` in `compose` is Rust's way of returning "some function" when the compiler can't know the exact type at compile time. For iterator chains, the compiler figures it all out and you never need that.

What This Unlocks

Key Differences

ConceptOCamlRust
Pipe operator`x \> f \> g` (built-in)No built-in โ€” use `.method()` chaining
Compose function`let compose f g x = f (g x)``fn compose(f, g) -> Box<dyn Fn...>`
Iterator pipeline`List.map f \> List.filter g``.map(f).filter(g).collect()`
LazinessEager by defaultLazy by default โ€” runs only when consumed
Returning a functionNatural (everything is a value)Need `impl Fn(...)` or `Box<dyn Fn(...) >`
/// Function Composition: building complex transformations from simple parts.
///
/// OCaml doesn't have a built-in composition operator (though `|>` serves similarly).
/// Rust also lacks one, but closures and method chaining achieve the same goal.

// โ”€โ”€ Compose two functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Compose f and g: returns a new function that applies g first, then f.
/// Equivalent to mathematical (f โˆ˜ g)(x) = f(g(x))
pub fn compose<A, B, C>(
    f: impl Fn(B) -> C + 'static,
    g: impl Fn(A) -> B + 'static,
) -> Box<dyn Fn(A) -> C> {
    Box::new(move |x| f(g(x)))
}

/// Pipe-style compose: applies f first, then g. More natural reading order.
/// Equivalent to OCaml's `x |> f |> g`
pub fn pipe<A, B, C>(
    f: impl Fn(A) -> B + 'static,
    g: impl Fn(B) -> C + 'static,
) -> Box<dyn Fn(A) -> C> {
    Box::new(move |x| g(f(x)))
}

// โ”€โ”€ Idiomatic Rust: method chaining via iterators โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Process a list: double, keep evens, sum. Demonstrates iterator composition.
pub fn process(data: &[i64]) -> i64 {
    data.iter()
        .map(|x| x * 2)
        .filter(|x| x % 2 == 0)
        .sum()
}

/// Build a transformation pipeline using fold over functions
pub fn pipeline(value: i64, steps: &[&dyn Fn(i64) -> i64]) -> i64 {
    steps.iter().fold(value, |acc, f| f(acc))
}

// โ”€โ”€ Compose multiple functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Compose a vector of functions into a single function (right-to-left)
pub fn compose_all(funcs: Vec<Box<dyn Fn(i64) -> i64>>) -> Box<dyn Fn(i64) -> i64> {
    Box::new(move |x| {
        funcs.iter().rev().fold(x, |acc, f| f(acc))
    })
}

/// Pipe a vector of functions (left-to-right)
pub fn pipe_all(funcs: Vec<Box<dyn Fn(i64) -> i64>>) -> Box<dyn Fn(i64) -> i64> {
    Box::new(move |x| {
        funcs.iter().fold(x, |acc, f| f(acc))
    })
}

// โ”€โ”€ Practical: string processing pipeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Compose string transformations
pub fn process_string(s: &str) -> String {
    // Rust's method chaining IS composition
    s.trim()
        .to_lowercase()
        .replace("  ", " ")
        .chars()
        .filter(|c| c.is_alphanumeric() || *c == ' ')
        .collect()
}

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

    #[test]
    fn test_compose() {
        let double = |x: i64| x * 2;
        let inc = |x: i64| x + 1;
        let double_then_inc = compose(inc, double); // inc(double(x))
        assert_eq!(double_then_inc(5), 11); // 5*2=10, +1=11
    }

    #[test]
    fn test_pipe() {
        let double = |x: i64| x * 2;
        let inc = |x: i64| x + 1;
        let inc_then_double = pipe(inc, double); // double(inc(x))
        assert_eq!(inc_then_double(5), 12); // 5+1=6, *2=12
    }

    #[test]
    fn test_process_iterator_chain() {
        assert_eq!(process(&[1, 2, 3, 4, 5]), 30); // all doubled are even
        assert_eq!(process(&[]), 0);
        assert_eq!(process(&[7]), 14);
    }

    #[test]
    fn test_pipeline() {
        let add1: &dyn Fn(i64) -> i64 = &|x| x + 1;
        let mul2: &dyn Fn(i64) -> i64 = &|x| x * 2;
        let sub3: &dyn Fn(i64) -> i64 = &|x| x - 3;
        assert_eq!(pipeline(5, &[add1, mul2, sub3]), 9); // (5+1)*2-3=9
    }

    #[test]
    fn test_compose_all() {
        let funcs: Vec<Box<dyn Fn(i64) -> i64>> = vec![
            Box::new(|x| x + 1),
            Box::new(|x| x * 2),
            Box::new(|x| x - 3),
        ];
        // Right-to-left: (x-3)*2+1; for x=10: 7*2+1=15
        let f = compose_all(funcs);
        assert_eq!(f(10), 15);
    }

    #[test]
    fn test_pipe_all() {
        let funcs: Vec<Box<dyn Fn(i64) -> i64>> = vec![
            Box::new(|x| x + 1),
            Box::new(|x| x * 2),
            Box::new(|x| x - 3),
        ];
        // Left-to-right: ((x+1)*2)-3; for x=10: 11*2-3=19
        let f = pipe_all(funcs);
        assert_eq!(f(10), 19);
    }

    #[test]
    fn test_process_string() {
        assert_eq!(process_string("  Hello  WORLD!  "), "hello world");
        assert_eq!(process_string(""), "");
        assert_eq!(process_string("Rust 2024"), "rust 2024");
    }
}

fn main() {
    println!("{:?}", double_then_inc(5), 11);
    println!("{:?}", inc_then_double(5), 12);
    println!("{:?}", process(&[1, 2, 3, 4, 5]), 30);
}
(* Function Composition: building pipelines from simple functions *)

(* โ”€โ”€ Composition operator (not built-in, but commonly defined) *)

let compose f g x = f (g x)   (* (f โˆ˜ g)(x) = f(g(x)) *)
let ( >> ) f g x = g (f x)    (* pipe/forward composition *)

(* โ”€โ”€ Basic examples โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let double x = x * 2
let inc x = x + 1
let square x = x * x

let double_then_inc = compose inc double   (* inc(double(x)) *)
let inc_then_double = inc >> double        (* double(inc(x)) *)

(* โ”€โ”€ Pipeline with |> (OCaml's most-used composition) โ”€โ”€โ”€โ”€ *)

let process data =
  data
  |> List.map (fun x -> x * 2)
  |> List.filter (fun x -> x mod 2 = 0)
  |> List.fold_left ( + ) 0

(* โ”€โ”€ Compose a list of functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let compose_all funcs x =
  List.fold_right (fun f acc -> f acc) funcs x

let pipe_all funcs x =
  List.fold_left (fun acc f -> f acc) x funcs

(* โ”€โ”€ Pipeline helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let pipeline value steps =
  List.fold_left (fun acc f -> f acc) value steps

(* โ”€โ”€ String processing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let process_string s =
  s
  |> String.trim
  |> String.lowercase_ascii

(* โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)
let () =
  assert (double_then_inc 5 = 11);
  assert (inc_then_double 5 = 12);
  assert (process [1;2;3;4;5] = 30);
  assert (process [] = 0);
  assert (pipeline 5 [inc; double; (fun x -> x - 3)] = 9);
  assert (compose_all [inc; double; (fun x -> x - 3)] 10 = 15);
  assert (pipe_all [inc; double; (fun x -> x - 3)] 10 = 19);
  assert (process_string "  Hello  " = "hello");
  print_endline "โœ“ All composition tests passed"

๐Ÿ“Š Detailed Comparison

Function Composition: OCaml vs Rust

The Core Insight

Composition is the essence of functional programming: build complex behavior by snapping together simple functions. OCaml expresses this through the pipe operator `|>` and custom composition operators. Rust achieves it through iterator method chaining, which is arguably even more natural for data processing pipelines.

OCaml Approach

OCaml's pipe operator `|>` is the workhorse for composition:
๐Ÿช Show OCaml equivalent
let result = data
|> List.map (fun x -> x * 2)
|> List.filter (fun x -> x > 5)
|> List.fold_left ( + ) 0
Custom composition operators are easy to define:
๐Ÿช Show OCaml equivalent
let compose f g x = f (g x)
let ( >> ) f g x = g (f x)
Functions compose naturally because of automatic currying โ€” `List.map f` returns a function ready to accept a list.

Rust Approach

Rust's iterator adapters ARE composition โ€” each method returns a new iterator:
let result: i64 = data.iter()
 .map(|x| x * 2)
 .filter(|x| x > &5)
 .sum();
For explicit function composition, closures do the job:
fn compose<A,B,C>(f: impl Fn(B)->C, g: impl Fn(A)->B) -> impl Fn(A)->C {
 move |x| f(g(x))
}
Rust's zero-cost abstractions mean iterator chains compile to the same code as hand-written loops.

Key Differences

AspectOCamlRust
Pipe operator`\>` (built-in)No equivalent (method chaining instead)
CompositionCustom `compose` / `>>`Closures or method chains
Iterator chains`List.map f \> List.filter g``.map(f).filter(g)`
Lazy evaluationLists are eagerIterators are lazy
Type inferenceFullUsually needs return type hints
PerformanceIntermediate lists allocatedZero-cost (fused into single pass)

What Rust Learners Should Notice

  • Method chaining replaces `|>`: Rust's `.map().filter().collect()` is the idiomatic equivalent of OCaml's pipe chains. It reads left-to-right and composes naturally.
  • Iterators are lazy: Unlike OCaml's `List.map` which creates a new list, Rust iterators don't allocate until `.collect()`. This is a major performance advantage.
  • Explicit composition is rare in Rust: While you can build `compose(f, g)`, idiomatic Rust prefers method chains or closures that call both functions inline.
  • `Box<dyn Fn>` vs `impl Fn`: Composing functions dynamically requires `Box<dyn Fn>` (heap allocation). Static composition with `impl Fn` is zero-cost but can't be stored in collections.

Further Reading

  • [The Rust Book โ€” Processing a Series of Items with Iterators](https://doc.rust-lang.org/book/ch13-02-iterators.html)
  • [OCaml Stdlib โ€” Fun module](https://v2.ocaml.org/api/Fun.html)
  • [Rust Iterator documentation](https://doc.rust-lang.org/std/iter/trait.Iterator.html)