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
- Data transformation pipelines โ ETL jobs, log processing, report generation: read โ clean โ filter โ aggregate in one readable chain
- Reusable validators โ compose `trim`, `lowercase`, `validate_length` into a single `normalize` function for any text input
- Configuration-driven transforms โ build a `Vec<Box<dyn Fn(i64) -> i64>>` at runtime and run any value through it with `fold`
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| 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()` | |
| Laziness | Eager by default | Lazy by default โ runs only when consumed | ||
| Returning a function | Natural (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๐ช Show OCaml equivalent
let compose f g x = f (g x)
let ( >> ) f g x = g (f x)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
| Aspect | OCaml | Rust | |
|---|---|---|---|
| Pipe operator | `\ | >` (built-in) | No equivalent (method chaining instead) |
| Composition | Custom `compose` / `>>` | Closures or method chains | |
| Iterator chains | `List.map f \ | > List.filter g` | `.map(f).filter(g)` |
| Lazy evaluation | Lists are eager | Iterators are lazy | |
| Type inference | Full | Usually needs return type hints | |
| Performance | Intermediate lists allocated | Zero-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)