947 Currying Partial
Tutorial
The Problem
Explore currying, partial application, and function sections in Rust. Unlike OCaml, where all functions are curried by default, Rust functions take all arguments at once. Partial application is achieved through closures that capture some arguments. Implement a curry converter, an uncurry converter, and a pipeline function that folds a value through a chain of unary functions.
🎯 Learning Outcomes
add_curried(x) -> impl Fn(i32) -> i32 to mimic OCaml's default curryingcurry<A, B, C> converter that turns fn(A, B) -> C into Fn(A) -> Box<dyn Fn(B) -> C>uncurry converterpipeline(init, &[&dyn Fn(i32) -> i32]) -> i32 as a fold over unary functionsCode Example
#![allow(clippy::all)]
/// 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);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Default currying | No — use closures | Yes — all multi-arg functions are auto-curried |
| Partial application | move |y| f(5, y) | f 5 — no closure syntax needed |
Box<dyn Fn> overhead | Required for generic curried return type | No equivalent overhead (closures are GC values) |
| Pipeline | fold over &[&dyn Fn] | List.fold_left (fun acc f -> f acc) |
| Operator sections | (|x| x * 2) | (( * ) 2) — more concise |
Rust's type system makes generic higher-order combinators like curry awkward — the A: Copy + 'static constraints are artifacts of ownership, not logic. For practical code, prefer direct closures over a generic curry combinator.
OCaml Approach
(* OCaml functions are curried by default *)
let add x y = x + y
let add5 = add 5 (* partial application — no closure needed *)
let add_curried x y = x + y (* identical to add *)
let pipeline initial funcs =
List.fold_left (fun acc f -> f acc) initial funcs
(* curry/uncurry as identity since OCaml is already curried *)
let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
In OCaml, add 5 is free — no closure allocation; the runtime tracks the partial application. curry and uncurry in OCaml convert between tuple-argument and curried-argument styles, which is the reverse of Rust's use case.
Full Source
#![allow(clippy::all)]
/// 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);
}
}#[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);
}
}
Deep 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
Box<dyn Fn>) but impl Fn closures are monomorphized and zero-costmove |y| ... captures variables by value — essential when the closure outlives its creation scopeimpl Fn(i32) -> i32 as a return type is Rust's way of saying "returns some closure" without boxing — it's a zero-cost abstractionFurther Reading
Exercises
compose(f, g) -> impl Fn(A) -> C where compose(f, g)(x) = g(f(x)) (left-to-right composition).compose_n(funcs: Vec<Box<dyn Fn(i32) -> i32>>) -> impl Fn(i32) -> i32 that composes a dynamic list of functions.add_curried to create add1, add10, add100 and apply them via pipeline.memoize higher-order function that wraps Fn(i32) -> i32 with a HashMap cache.FnOnce, FnMut, Fn distinction: write examples that compile only with FnOnce and only with Fn.