074 — Currying and Partial Application (Applied)
Tutorial
The Problem
This example applies currying and partial application to practical patterns — building factories, greeting generators, and transformation pipelines. Where example 005 introduced the theory, this example shows the patterns in context: make_adder(5) returns a reusable function, apply_twice demonstrates function composition from first principles.
Partial application is ubiquitous in functional programming: event handler factories in UIs, middleware pipelines in web frameworks, predicate factories in query builders, and configuration-bound operations. Understanding how to build and compose these in Rust is essential for ergonomic API design.
🎯 Learning Outcomes
make_adder(n) -> impl Fn(i32) -> i32apply_twice to demonstrate functions as valuescompose to build pipelinesmove keyword for capturing environment in closuresCode Example
//! 074: Currying and Partial Application
//!
//! Rust doesn't curry automatically, so we use closures that capture their
//! arguments by `move` and return `impl Fn(_) -> _`. Each factory returns a
//! reusable, pre-configured function — the Rust analogue of OCaml's partial
//! application.
/// Factory: returns a closure that adds `x` to its argument.
pub fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
/// Factory: returns a closure that multiplies its argument by `x`.
pub fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
/// Two-level curried greeting builder:
/// `make_greeting("Hello")("World")("!") == "Hello World!"`.
pub fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String> {
let prefix = prefix.to_string();
move |name: &str| {
let prefix = prefix.clone();
let name = name.to_string();
Box::new(move |suffix: &str| format!("{} {}{}", prefix, name, suffix))
}
}
/// Apply `f` twice: `f(f(x))`.
pub fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
/// Function composition: returns a closure computing `f(g(x))`.
pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_application_with_adder() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
}
#[test]
fn partial_application_with_multiplier() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
assert_eq!(double(7), 14);
assert_eq!(triple(4), 12);
}
#[test]
fn multi_level_curried_greeting() {
let hello = make_greeting("Hello");
let hello_world = hello("World");
assert_eq!(hello_world("!"), "Hello World!");
}
#[test]
fn apply_twice_reuses_partial_functions() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn compose_orders_matter() {
let add5 = make_adder(5);
let double = make_multiplier(2);
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3 + 5) * 2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3 * 2 + 5
// Closures remain usable after composition (they're by-reference bounds).
assert_eq!(add5(1), 6);
assert_eq!(double(1), 2);
}
}Key Differences
move requirement**: Rust requires move in the closure to capture x by value. OCaml captures by closure environment automatically (GC-managed). Without move, Rust would borrow x — problematic when the closure outlives the function.impl Fn return type**: Rust's -> impl Fn(i32) -> i32 return type is required because closures have unique anonymous types. OCaml's fun x -> ... return type is inferred as int -> int.move), so it has 'static lifetime. OCaml closures can safely reference the outer scope due to GC.compose(f, g) in Rust requires + 'static bounds if stored. OCaml's fun x -> f (g x) composes naturally.OCaml Approach
OCaml functions are automatically curried, so partial application is natural:
let make_adder x y = x + y (* equivalent to: let make_adder x = fun y -> x + y *)
let add5 = make_adder 5 (* partial application: bind x=5 *)
let _ = add5 3 (* evaluates to 8 *)
let apply_twice f x = f (f x)
let compose f g = fun x -> f (g x)
let double_then_add5 = compose add5 (fun x -> x * 2)
OCaml's implicit currying means every multi-argument function is already a curried function. Partial application is just function application with fewer arguments than the full arity.
Full Source
//! 074: Currying and Partial Application
//!
//! Rust doesn't curry automatically, so we use closures that capture their
//! arguments by `move` and return `impl Fn(_) -> _`. Each factory returns a
//! reusable, pre-configured function — the Rust analogue of OCaml's partial
//! application.
/// Factory: returns a closure that adds `x` to its argument.
pub fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
/// Factory: returns a closure that multiplies its argument by `x`.
pub fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
/// Two-level curried greeting builder:
/// `make_greeting("Hello")("World")("!") == "Hello World!"`.
pub fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String> {
let prefix = prefix.to_string();
move |name: &str| {
let prefix = prefix.clone();
let name = name.to_string();
Box::new(move |suffix: &str| format!("{} {}{}", prefix, name, suffix))
}
}
/// Apply `f` twice: `f(f(x))`.
pub fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
/// Function composition: returns a closure computing `f(g(x))`.
pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_application_with_adder() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
}
#[test]
fn partial_application_with_multiplier() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
assert_eq!(double(7), 14);
assert_eq!(triple(4), 12);
}
#[test]
fn multi_level_curried_greeting() {
let hello = make_greeting("Hello");
let hello_world = hello("World");
assert_eq!(hello_world("!"), "Hello World!");
}
#[test]
fn apply_twice_reuses_partial_functions() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn compose_orders_matter() {
let add5 = make_adder(5);
let double = make_multiplier(2);
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3 + 5) * 2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3 * 2 + 5
// Closures remain usable after composition (they're by-reference bounds).
assert_eq!(add5(1), 6);
assert_eq!(double(1), 2);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_application_with_adder() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
}
#[test]
fn partial_application_with_multiplier() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
assert_eq!(double(7), 14);
assert_eq!(triple(4), 12);
}
#[test]
fn multi_level_curried_greeting() {
let hello = make_greeting("Hello");
let hello_world = hello("World");
assert_eq!(hello_world("!"), "Hello World!");
}
#[test]
fn apply_twice_reuses_partial_functions() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn compose_orders_matter() {
let add5 = make_adder(5);
let double = make_multiplier(2);
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3 + 5) * 2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3 * 2 + 5
// Closures remain usable after composition (they're by-reference bounds).
assert_eq!(add5(1), 6);
assert_eq!(double(1), 2);
}
}
Deep Comparison
Core Insight
In OCaml, let add x y = x + y is actually let add = fun x -> fun y -> x + y. Partial application (add 5) returns a function. Rust functions aren't curried — you use closures to simulate partial application.
OCaml Approach
let add x y = x + y → add 5 returns fun y -> 5 + yRust Approach
let add5 = |y| 5 + y;move closures for ownership transferimpl Fn(T) -> UComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Curried by default | Yes | No |
| Partial application | f x (give fewer args) | Closure capturing |
| Return function | Automatic | impl Fn(T) -> U |
| Closure syntax | fun x -> ... | \|x\| ... |
Exercises
make_tax_calculator(rate: f64) -> impl Fn(f64) -> f64 and make_discount(pct: f64) -> impl Fn(f64) -> f64. Compose them into a calculate_price pipeline.memoized_make_adder(cache: &mut HashMap<i32, Box<dyn Fn(i32) -> i32>>, n: i32) -> &Box<dyn Fn(i32) -> i32> that caches the created adder function.compose, build a pipeline for data transformation: trim -> lowercase -> split_words -> filter_short_words -> join_with_comma. Each step is a partial application of a generic combinator.