005 — Currying and Partial Application
Tutorial Video
Text description (accessibility)
This video demonstrates the "005 — Currying and Partial Application" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Currying (named after mathematician Haskell Curry, though independently discovered by Moses Schonfinkel) is the technique of transforming a multi-argument function into a chain of single-argument functions. Key difference from OCaml: 1. **Default currying**: OCaml curries all functions automatically; every `let f a b = ...` is implicitly `let f = fun a
Tutorial
The Problem
Currying (named after mathematician Haskell Curry, though independently discovered by Moses Schonfinkel) is the technique of transforming a multi-argument function into a chain of single-argument functions. add(a, b) becomes add(a)(b), where add(5) returns a new function that adds 5 to its argument. This makes partial application — fixing some arguments now and the rest later — a natural consequence of function application.
Currying is the foundation of functional composition. It enables point-free style, makes predicate factories (like greater_than(threshold)) trivial, and is how Haskell and OCaml's standard libraries are designed. In practice it appears in React's higher-order components, Lodash's _.curry, and functional Redux reducers.
🎯 Learning Outcomes
move closures to capture environment by valueList.map, List.filter, and List.fold_left all take their function argument first for partial applicationCode Example
fn add(n: i64) -> impl Fn(i64) -> i64 {
move |x| x + n
}
let add5 = add(5);Key Differences
let f a b = ... is implicitly let f = fun a -> fun b -> .... Rust requires explicit closure-returning functions.fun x -> x + n. Rust: move |x| x + n. The move keyword is required in Rust to transfer ownership of captured variables into the closure.impl Fn(T) -> T return type must be written explicitly when returning closures from functions.FnOnce vs Fn**: Rust distinguishes closures that can be called once (FnOnce), once at a time (FnMut), and multiple times (Fn). OCaml has no such distinction.add 5 as int -> int without annotation. Rust requires impl Fn(i64) -> i64 as the return type annotation.move. OCaml's closures capture implicitly with GC ownership.Clone for reuse:** Rust's partial requires A: Clone because the captured argument may be used multiple times. OCaml doesn't need this — sharing is automatic.Box<dyn Fn> for dynamic dispatch:** Returning closures from functions sometimes requires Box<dyn Fn> in Rust when the concrete type is unnameable. OCaml closures always erase to a uniform representation.OCaml Approach
In OCaml, ALL multi-argument functions are automatically curried. let add a b = a + b is syntactic sugar for let add = fun a -> fun b -> a + b. Partial application is free: let add5 = add 5 immediately produces a function. let double = ( * ) 2 partially applies multiplication. The standard library exploits this everywhere: List.map (fun x -> x + 1) partially applies List.map.
Full Source
#![allow(clippy::all)]
/// Currying and Partial Application: OCaml's default vs Rust's closures.
///
/// In OCaml, ALL functions are curried by default. `let add a b = a + b` is
/// really `let add = fun a -> fun b -> a + b`. Rust doesn't curry automatically,
/// but closures achieve the same effect.
// ── Idiomatic Rust: closures for partial application ────────────────────────
/// Returns a closure that adds `n` to its argument.
/// This is the Rust equivalent of OCaml's `let add n = fun x -> x + n`
pub fn add(n: i64) -> impl Fn(i64) -> i64 {
move |x| x + n
}
/// Returns a closure that multiplies by `n`
pub fn multiply_by(n: i64) -> impl Fn(i64) -> i64 {
move |x| x * n
}
/// Generic partial application: fix the first argument of a two-arg function.
/// In OCaml this is free; in Rust we build it explicitly.
pub fn partial<A, B, C, F>(f: F, a: A) -> impl Fn(B) -> C
where
F: Fn(A, B) -> C,
A: Clone,
{
move |b| f(a.clone(), b)
}
// ── Demonstrating curried-style APIs ────────────────────────────────────────
/// A curried comparison: returns a predicate
pub fn greater_than(threshold: i64) -> impl Fn(&i64) -> bool {
move |x| *x > threshold
}
/// Apply a list of transformations (each is a curried function result)
pub fn apply_all(value: i64, transforms: &[Box<dyn Fn(i64) -> i64>]) -> i64 {
transforms.iter().fold(value, |acc, f| f(acc))
}
// ── Functional style: simulating currying with nested closures ──────────────
/// Fully curried three-argument function
/// OCaml: `let f a b c = a + b + c` → `f 1` returns a function, `f 1 2` returns a function
pub fn curried_add3(a: i64) -> Box<dyn Fn(i64) -> Box<dyn Fn(i64) -> i64>> {
Box::new(move |b| Box::new(move |c| a + b + c))
}
// ── Practical example: building predicates ──────────────────────────────────
/// Filter with a curried predicate builder
pub fn between(low: i64, high: i64) -> impl Fn(&i64) -> bool {
move |x| *x >= low && *x <= high
}
pub fn filter_between(list: &[i64], low: i64, high: i64) -> Vec<i64> {
list.iter().copied().filter(between(low, high)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_partial() {
let add5 = add(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_multiply_by() {
let double = multiply_by(2);
let triple = multiply_by(3);
assert_eq!(double(7), 14);
assert_eq!(triple(7), 21);
}
#[test]
fn test_generic_partial() {
let pow = |base: i64, exp: u32| base.pow(exp);
let square = partial(pow, 2_i64); // partially apply base=2
// Note: this gives 2^exp, not x^2
assert_eq!(square(10), 1024); // 2^10
}
#[test]
fn test_greater_than_as_predicate() {
let data = vec![1, 5, 3, 8, 2, 9];
let big: Vec<_> = data
.iter()
.filter(|x| greater_than(4)(x))
.copied()
.collect();
assert_eq!(big, vec![5, 8, 9]);
}
#[test]
fn test_curried_add3() {
let f = curried_add3(1);
let g = f(2);
assert_eq!(g(3), 6);
assert_eq!(curried_add3(10)(20)(30), 60);
}
#[test]
fn test_filter_between() {
assert_eq!(filter_between(&[1, 2, 3, 4, 5, 6], 2, 5), vec![2, 3, 4, 5]);
assert_eq!(filter_between(&[], 0, 10), Vec::<i64>::new());
assert_eq!(filter_between(&[100], 0, 10), Vec::<i64>::new());
}
#[test]
fn test_apply_all_transforms() {
let transforms: Vec<Box<dyn Fn(i64) -> i64>> = vec![
Box::new(add(10)),
Box::new(multiply_by(2)),
Box::new(add(-1)),
];
// 5 → +10 = 15 → *2 = 30 → -1 = 29
assert_eq!(apply_all(5, &transforms), 29);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_partial() {
let add5 = add(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_multiply_by() {
let double = multiply_by(2);
let triple = multiply_by(3);
assert_eq!(double(7), 14);
assert_eq!(triple(7), 21);
}
#[test]
fn test_generic_partial() {
let pow = |base: i64, exp: u32| base.pow(exp);
let square = partial(pow, 2_i64); // partially apply base=2
// Note: this gives 2^exp, not x^2
assert_eq!(square(10), 1024); // 2^10
}
#[test]
fn test_greater_than_as_predicate() {
let data = vec![1, 5, 3, 8, 2, 9];
let big: Vec<_> = data
.iter()
.filter(|x| greater_than(4)(x))
.copied()
.collect();
assert_eq!(big, vec![5, 8, 9]);
}
#[test]
fn test_curried_add3() {
let f = curried_add3(1);
let g = f(2);
assert_eq!(g(3), 6);
assert_eq!(curried_add3(10)(20)(30), 60);
}
#[test]
fn test_filter_between() {
assert_eq!(filter_between(&[1, 2, 3, 4, 5, 6], 2, 5), vec![2, 3, 4, 5]);
assert_eq!(filter_between(&[], 0, 10), Vec::<i64>::new());
assert_eq!(filter_between(&[100], 0, 10), Vec::<i64>::new());
}
#[test]
fn test_apply_all_transforms() {
let transforms: Vec<Box<dyn Fn(i64) -> i64>> = vec![
Box::new(add(10)),
Box::new(multiply_by(2)),
Box::new(add(-1)),
];
// 5 → +10 = 15 → *2 = 30 → -1 = 29
assert_eq!(apply_all(5, &transforms), 29);
}
}
Deep Comparison
Currying and Partial Application: OCaml vs Rust
The Core Insight
Currying is automatic and pervasive in OCaml — every function is curried, and partial application is free. Rust requires explicit closures, but the resulting pattern is equally powerful. Understanding this difference reveals how each language thinks about functions as values.
OCaml Approach
In OCaml, let add a b = a + b is syntactic sugar for let add = fun a -> fun b -> a + b. Every multi-argument function is actually a chain of single-argument functions:
let add5 = add 5 (* partially apply: returns fun b -> 5 + b *)
let result = add5 3 (* 8 *)
let big = List.filter (greater_than 4) [1;5;3;8] (* [5;8] *)
This makes function composition and predicate building incredibly natural. The |> pipe operator complements currying beautifully.
Rust Approach
Rust functions are not automatically curried. To achieve partial application, you return closures:
fn add(n: i64) -> impl Fn(i64) -> i64 {
move |x| x + n
}
let add5 = add(5);
The move keyword captures n by value (ownership transfer). For generic partial application, you need explicit lifetime and trait bounds — more verbose but equally expressive.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Currying | Automatic (all functions) | Manual (return closures) |
| Partial application | Free (f a) | Explicit (move \|x\| ...) |
| Closure capture | Automatic (GC) | move or borrow (ownership) |
| Function types | 'a -> 'b -> 'c | Fn(A) -> impl Fn(B) -> C |
| Type inference | Full | Partial (return types need annotation) |
| Performance | Allocation per partial app | Zero-cost when inlined |
| Trait bounds | None needed | Fn/FnMut/FnOnce |
What Rust Learners Should Notice
Fn vs FnMut vs FnOnce**: These three traits determine how a closure captures its environment. Fn borrows immutably, FnMut borrows mutably, FnOnce consumes. OCaml doesn't have this distinction (GC handles it).impl Fn vs Box<dyn Fn>**: impl Fn is monomorphized (zero-cost, can't be stored in collections). Box<dyn Fn> is heap-allocated (can be stored, has runtime cost). OCaml closures are always heap-allocated.move is the key**: Without move, closures borrow from their environment. With move, they take ownership. This is the ownership system at work in closures.Further Reading
Exercises
make_pipeline(steps: Vec<Box<dyn Fn(i64) -> i64>>) -> impl Fn(i64) -> i64 that returns a function composing all steps in sequence.partial that memoizes the result of calling the inner function with a particular a value using a HashMap.uncurry<A,B,C>(f: impl Fn(A) -> impl Fn(B) -> C) -> impl Fn(A, B) -> C that converts a curried function back into a two-argument function.once_with(f: impl FnOnce() -> T) -> impl Fn() -> T that calls f once and caches the result (hint: use std::cell::OnceCell).uncurry<A, B, C>(f: impl Fn(A) -> impl Fn(B) -> C) -> impl Fn(A, B) -> C that converts a curried function back to a two-argument function.