074: Currying and Partial Application
Difficulty: 2 Level: Beginner OCaml curries everything automatically. Rust doesn't โ but closures capture arguments explicitly with equal power and more transparency.The Problem This Solves
You want to reuse a multi-argument function with one argument fixed: an adder with a fixed offset, a comparator with a fixed threshold, a formatter with a fixed template. In a higher-order pipeline, you need to produce a single-argument function from a multi-argument one. In OCaml this is free: `let add5 = add 5` partially applies `add` instantly. In Rust you write `let add5 = |y| add(5, y)` โ explicit, but it makes the capture visible. The power is the same; the ceremony differs.The Intuition
Currying means every function takes exactly one argument and returns either a value or another function. `add 5 3` is really `(add 5) 3` โ apply `add` to `5`, get a new function, apply that to `3`. Rust's closures capture bindings explicitly: `move |y| x + y` captures `x` by value and takes `y` as argument. This is manual currying โ more explicit but also clearer about what is captured. The `move` keyword is the Rust equivalent of OCaml's value capture โ it ensures the closure owns its captured variables and can outlive the scope that created them.How It Works in Rust
// Regular two-argument function โ NOT curried
pub fn add(x: i32, y: i32) -> i32 { x + y }
// Partial application via closure โ the idiomatic Rust way
pub fn add5() -> impl Fn(i32) -> i32 {
move |y| add(5, y) // captures 5, takes y as argument
}
// Curried form: takes x, returns a function that takes y
pub fn add_curried(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // `move` captures x by value into the closure
}
// Operator sections via closures
pub fn double() -> impl Fn(i32) -> i32 { |x| x * 2 }
pub fn increment() -> impl Fn(i32) -> i32 { |x| x + 1 }
// Use them in a pipeline
fn pipeline(data: &[i32]) -> Vec<i32> {
data.iter()
.copied()
.map(add_curried(10)) // partial: add 10 to each
.map(double()) // double each result
.collect()
}
// Generic curry converter: (A, B) -> C becomes A -> (B -> C)
pub fn curry<A, B, C>(f: impl Fn(A, B) -> C + 'static) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where A: Copy + 'static, B: 'static, C: 'static {
move |a| Box::new(move |b| f(a, b))
}
// Compose two functions: g(f(x))
pub fn compose<A, B, C>(
f: impl Fn(A) -> B,
g: impl Fn(B) -> C,
) -> impl Fn(A) -> C {
move |x| g(f(x))
}
Note: returning `impl Fn(i32) -> i32` works for simple cases. For storing in a struct or returning from a trait object, use `Box<dyn Fn(i32) -> i32>`.
What This Unlocks
- Pipeline factories: generate configured `.map()` functions with fixed parameters.
- Dependency injection light: pass partially-applied functions instead of full objects.
- Point-free style: compose operations without naming intermediate values.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Default currying | Automatic โ every function | Not automatic โ must use closures | ||
| Partial apply | `let add5 = add 5` | `let add5 = \ | y\ | add(5, y)` |
| Capture | Implicit (lexical scope) | Explicit `move` for owned capture | ||
| Compose | `\ | >` pipe or `Fun.compose` | Custom `compose()` or `\ | >` macro |
| Return type | `int -> int` | `impl Fn(i32) -> i32` or `Box<dyn Fn>` |
/// Currying, Partial Application, and Sections
///
/// OCaml functions are curried by default: `let add x y = x + y` can be
/// partially applied as `add 5`. Rust functions are NOT curried โ closures
/// are used instead for partial application.
/// Regular two-argument function (NOT curried in Rust).
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
/// Partial application via closure โ the Rust way.
pub fn add5() -> impl Fn(i32) -> i32 {
move |y| add(5, y)
}
/// Curried function โ returns a closure. This mimics OCaml's default.
pub fn add_curried(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
/// Operator "sections" via closures.
pub fn double() -> impl Fn(i32) -> i32 {
|x| x * 2
}
pub fn increment() -> impl Fn(i32) -> i32 {
|x| x + 1
}
pub fn halve() -> impl Fn(i32) -> i32 {
|x| x / 2
}
/// Curry converter: turns a 2-arg function into a curried one.
/// Requires A: Copy so the closure can capture it by value in Fn.
pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
A: Copy + 'static,
B: 'static,
C: 'static,
F: Fn(A, B) -> C + Clone + 'static,
{
move |a: A| {
let f = f.clone();
Box::new(move |b: B| f(a, b))
}
}
/// Uncurry converter: turns a curried function into a 2-arg one.
pub fn uncurry<A, B, C>(f: impl Fn(A) -> Box<dyn Fn(B) -> C>) -> impl Fn(A, B) -> C {
move |a, b| f(a)(b)
}
/// Pipeline: fold a value through a list of functions.
pub fn pipeline(initial: i32, funcs: &[&dyn Fn(i32) -> i32]) -> i32 {
funcs.iter().fold(initial, |acc, f| f(acc))
}
/// Scale and shift with named parameters (Rust doesn't have labeled args,
/// but builder pattern or structs serve the same purpose).
pub fn scale_and_shift(scale: i32, shift: i32, x: i32) -> i32 {
x * scale + shift
}
pub fn celsius_of_fahrenheit(f: i32) -> i32 {
scale_and_shift(5, -160, f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add5() {
assert_eq!(add5()(10), 15);
}
#[test]
fn test_curried() {
let add3 = add_curried(3);
assert_eq!(add3(7), 10);
assert_eq!(add3(0), 3);
}
#[test]
fn test_sections() {
assert_eq!(double()(7), 14);
assert_eq!(increment()(9), 10);
assert_eq!(halve()(20), 10);
}
#[test]
fn test_pipeline() {
let d = double();
let i = increment();
let h = halve();
let result = pipeline(6, &[&d, &i, &h]);
// 6 * 2 = 12, + 1 = 13, / 2 = 6
assert_eq!(result, 6);
}
#[test]
fn test_celsius() {
assert_eq!(celsius_of_fahrenheit(212), 900); // 212*5 - 160 = 900
// Note: integer arithmetic, not actual Celsius conversion
}
#[test]
fn test_curry_uncurry() {
let curried_add = curry(add);
assert_eq!(curried_add(3)(4), 7);
}
}
let add x y = x + y
let add5 = add 5
let add_tup (x, y) = x + y
let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
let double = ( * ) 2
let increment = ( + ) 1
let halve = Fun.flip ( / ) 2
let scale_and_shift ~scale ~shift x = x * scale + shift
let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)
let () =
assert (add5 10 = 15);
assert (double 7 = 14);
assert (halve 20 = 10);
let pipeline = [double; increment; halve] in
let result = List.fold_left (fun acc f -> f acc) 6 pipeline in
assert (result = 6);
print_endline "All assertions passed."
๐ Detailed Comparison
Currying, Partial Application, and Sections: OCaml vs Rust
The Core Insight
Currying is OCaml's bread and butter โ every multi-argument function is actually a chain of single-argument functions. Rust made a deliberate design choice NOT to curry by default, favoring explicit closures instead. This reveals a philosophical difference: OCaml optimizes for functional composition; Rust optimizes for clarity and zero-cost abstraction.OCaml Approach
In OCaml, `let add x y = x + y` is syntactic sugar for `let add = fun x -> fun y -> x + y`. This means `add 5` naturally returns a function `fun y -> 5 + y`. Operator sections like `( * ) 2` partially apply multiplication. `Fun.flip` swaps argument order for operators like division. Labeled arguments (`~scale ~shift`) enable partial application in any order. This all composes beautifully for pipeline-style programming.Rust Approach
Rust functions take all arguments at once: `fn add(x: i32, y: i32) -> i32`. Partial application requires explicitly returning a closure: `fn add5() -> impl Fn(i32) -> i32 { |y| add(5, y) }`. The `move` keyword captures variables by value. Generic `curry`/`uncurry` converters are possible but require `Box<dyn Fn>` for the intermediate closure (due to Rust's requirement that function return types have known size). The tradeoff is more verbosity for complete control over capture and allocation.Side-by-Side
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Default | All functions curried | All functions take full args | ||
| Partial application | `add 5` (free) | `\ | y\ | add(5, y)` (closure) |
| Operator section | `( ) 2` | `\ | x\ | x 2` |
| Flip | `Fun.flip ( / ) 2` | No built-in (write closure) | ||
| Labeled args | `~scale ~shift` | Not available (use structs) | ||
| Pipeline | `List.fold_left` | `iter().fold()` or explicit | ||
| Return function | Natural (currying) | `impl Fn(...)` or `Box<dyn Fn(...)>` |
What Rust Learners Should Notice
- OCaml's currying is zero-cost because the compiler knows the full type; Rust closures may allocate when boxed (`Box<dyn Fn>`) but `impl Fn` closures are monomorphized and zero-cost
- `move |y| ...` captures variables by value โ essential when the closure outlives its creation scope
- Rust's lack of currying is intentional: explicit closures are clearer about what's captured and how
- `impl Fn(i32) -> i32` as a return type is Rust's way of saying "returns some closure" without boxing โ it's a zero-cost abstraction
- For labeled/named arguments, Rust uses the builder pattern or struct arguments โ different idiom, same result
Further Reading
- [The Rust Book โ Closures](https://doc.rust-lang.org/book/ch13-01-closures.html)
- [OCaml Higher-Order Functions](https://cs3110.github.io/textbook/chapters/hop/higher_order.html)