Currying and Partial Application
Tutorial Video
Text description (accessibility)
This video demonstrates the "Currying and Partial Application" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functions, Higher-Order Programming. Implement `let add x y = x + y` in Rust, showing how OCaml's automatic currying translates to Rust's explicit closures for partial application. Key difference from OCaml: 1. **Currying:** OCaml auto
Tutorial
The Problem
Implement let add x y = x + y in Rust, showing how OCaml's automatic currying translates to Rust's explicit closures for partial application.
🎯 Learning Outcomes
impl Fn return typesmove closures to capture values for partial applicationAdd + Copy)curry / uncurry combinators as higher-order functions🦀 The Rust Way
Rust functions take all arguments at once. To get partial application, you return a closure: fn add_partial(x: i64) -> impl Fn(i64) -> i64. The move keyword transfers ownership of captured values into the closure.
Code Example
/// Plain two-argument function.
fn add(x: i64, y: i64) -> i64 {
x + y
}
/// Partial application: return a closure capturing `x`.
fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
move |y| x + y
}
let add5 = add_partial(5);
assert_eq!(add5(3), 8);Key Differences
int -> int -> int; Rust needs impl Fn(i64) -> i64 return typemove for ownership transfer<T: Add + Copy> boundsOCaml Approach
In OCaml, let add x y = x + y is sugar for let add = fun x -> fun y -> x + y. Every function is automatically curried — add 5 returns a function. No special syntax needed for partial application.
Full Source
#![allow(clippy::all)]
// # Currying and Partial Application
//
// OCaml: `let add x y = x + y` — all functions are automatically curried.
// Rust has no auto-currying, but closures make partial application natural.
// ---------------------------------------------------------------------------
// Solution 1: Idiomatic Rust — closures for partial application
// ---------------------------------------------------------------------------
/// A plain two-argument function — Rust's default style.
pub fn add(x: i64, y: i64) -> i64 {
x + y
}
/// Returns a closure that adds `x` to its argument.
/// This is how Rust developers do partial application: return a closure.
pub fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
move |y| x + y
}
// ---------------------------------------------------------------------------
// Solution 2: Curried style — mirrors OCaml's `let add x y = x + y`
// ---------------------------------------------------------------------------
/// Fully curried: each argument returns a closure expecting the next.
/// Closest to OCaml's automatic currying, but explicit in Rust.
pub fn add_curried(x: i64) -> impl Fn(i64) -> i64 {
move |y| x + y
}
/// A generic curried add for any type supporting `Add`.
/// Shows how Rust generics replace OCaml's polymorphism here.
pub fn add_curried_generic<T>(x: T) -> impl Fn(T) -> T
where
T: std::ops::Add<Output = T> + Copy,
{
move |y| x + y
}
// ---------------------------------------------------------------------------
// Solution 3: Higher-order — curry any two-argument function
// ---------------------------------------------------------------------------
/// Transforms a two-argument function into a curried chain.
/// `curry(f)` returns `|x| |y| f(x, y)`.
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 + Copy + 'static,
{
move |a: A| Box::new(move |b: B| f(a, b))
}
/// The inverse: uncurry a curried function back to a two-argument function.
pub fn uncurry<A, B, C, F, G>(f: F) -> impl Fn(A, B) -> C
where
F: Fn(A) -> G,
G: Fn(B) -> C,
{
move |a, b| f(a)(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_direct() {
assert_eq!(add(3, 4), 7);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_partial_application() {
let add5 = add_partial(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_curried() {
let add10 = add_curried(10);
assert_eq!(add10(1), 11);
assert_eq!(add10(-10), 0);
// Call in one shot: add_curried(2)(3)
assert_eq!(add_curried(2)(3), 5);
}
#[test]
fn test_curried_generic() {
let add_f64 = add_curried_generic(1.5_f64);
assert!((add_f64(2.5) - 4.0).abs() < f64::EPSILON);
let add_i32 = add_curried_generic(100_i32);
assert_eq!(add_i32(23), 123);
}
#[test]
fn test_curry_combinator() {
let curried_add = curry(add);
let add7 = curried_add(7);
assert_eq!(add7(3), 10);
assert_eq!(add7(0), 7);
}
#[test]
fn test_uncurry_combinator() {
let uncurried = uncurry(add_curried);
assert_eq!(uncurried(3, 4), 7);
assert_eq!(uncurried(0, 0), 0);
}
#[test]
fn test_curry_with_multiply() {
let mul = |a: i64, b: i64| a * b;
let curried_mul = curry(mul);
let double = curried_mul(2);
assert_eq!(double(5), 10);
assert_eq!(double(0), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_direct() {
assert_eq!(add(3, 4), 7);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_partial_application() {
let add5 = add_partial(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_curried() {
let add10 = add_curried(10);
assert_eq!(add10(1), 11);
assert_eq!(add10(-10), 0);
// Call in one shot: add_curried(2)(3)
assert_eq!(add_curried(2)(3), 5);
}
#[test]
fn test_curried_generic() {
let add_f64 = add_curried_generic(1.5_f64);
assert!((add_f64(2.5) - 4.0).abs() < f64::EPSILON);
let add_i32 = add_curried_generic(100_i32);
assert_eq!(add_i32(23), 123);
}
#[test]
fn test_curry_combinator() {
let curried_add = curry(add);
let add7 = curried_add(7);
assert_eq!(add7(3), 10);
assert_eq!(add7(0), 7);
}
#[test]
fn test_uncurry_combinator() {
let uncurried = uncurry(add_curried);
assert_eq!(uncurried(3, 4), 7);
assert_eq!(uncurried(0, 0), 0);
}
#[test]
fn test_curry_with_multiply() {
let mul = |a: i64, b: i64| a * b;
let curried_mul = curry(mul);
let double = curried_mul(2);
assert_eq!(double(5), 10);
assert_eq!(double(0), 0);
}
}
Deep Comparison
OCaml vs Rust: Currying and Partial Application
Side-by-Side Code
OCaml
(* All functions are automatically curried *)
let add x y = x + y
(* Partial application — just supply fewer arguments *)
let add5 = add 5
(* Equivalent desugared form *)
let add' = fun x -> fun y -> x + y
Rust (idiomatic)
/// Plain two-argument function.
fn add(x: i64, y: i64) -> i64 {
x + y
}
/// Partial application: return a closure capturing `x`.
fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
move |y| x + y
}
let add5 = add_partial(5);
assert_eq!(add5(3), 8);
Rust (functional/recursive)
/// Fully curried — mirrors OCaml's `fun x -> fun y -> x + y`.
fn add_curried(x: i64) -> impl Fn(i64) -> i64 {
move |y| x + y
}
/// Generic version with trait bounds.
fn add_curried_generic<T: std::ops::Add<Output = T> + Copy>(x: T) -> impl Fn(T) -> T {
move |y| x + y
}
// One-shot call reads like OCaml: add_curried(2)(3)
assert_eq!(add_curried(2)(3), 5);
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Function signature | val add : int -> int -> int | fn add(x: i64, y: i64) -> i64 |
| Curried signature | int -> int -> int (same!) | fn(i64) -> impl Fn(i64) -> i64 |
| Partial application | let add5 = add 5 | let add5 = add_partial(5) |
| Polymorphic | val add : 'a -> 'a -> 'a (with (+)) | fn add<T: Add<Output=T> + Copy>(x: T) -> impl Fn(T) -> T |
| Closure type | Implicit, GC-managed | impl Fn(i64) -> i64 (stack or heap via Box<dyn Fn>) |
| Curry combinator | val curry : ('a * 'b -> 'c) -> 'a -> 'b -> 'c | fn curry<A,B,C,F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C> |
Key Insights
let add x y = x + y is already fun x -> fun y -> x + y. In Rust, you must explicitly return a closure to achieve the same effect.move closures are Rust's capture mechanism.** Where OCaml's GC handles closure environments automatically, Rust's move |y| x + y transfers ownership of x into the closure — no garbage collection needed.impl Fn vs Box<dyn Fn> — static vs dynamic dispatch.** When the caller knows the concrete closure type, impl Fn gives zero-cost abstraction. When you need to store or return closures of varying types (e.g., from the curry combinator), Box<dyn Fn> provides dynamic dispatch.(+) works on any numeric type via ad-hoc polymorphism. Rust requires T: Add<Output = T> + Copy to express the same constraint — more verbose but explicit.move closures with impl Fn return types are monomorphized at compile time — no allocation, no indirection.When to Use Each Style
Use idiomatic Rust when: You have a fixed-arity function and occasionally want partial application — add_partial(5) is clear and efficient.
Use curried Rust when: You're building combinator libraries or DSLs where function composition and point-free style improve readability, e.g., let transform = compose(scale(2.0), translate(1.0)).
Exercises
add3 function i32 -> i32 -> i32 -> i32 and use partial application to create an increment and a add10 specialization.flip — a higher-order function that takes f: A -> B -> C and returns B -> A -> C — and use it to partially apply the second argument of a binary function.format_field that accepts a padding width, an alignment, and finally a value, returning the formatted string.