Closure Currying
Tutorial Video
Text description (accessibility)
This video demonstrates the "Closure Currying" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Currying (named after Haskell Curry, formalised by SchΓΆnfinkel) is the theoretical foundation of lambda calculus β every multi-argument function can be expressed as nested single-argument functions. Key difference from OCaml: 1. **Implicit currying**: OCaml's `add x y` is syntactic sugar for `fun x
Tutorial
The Problem
Currying (named after Haskell Curry, formalised by SchΓΆnfinkel) is the theoretical foundation of lambda calculus β every multi-argument function can be expressed as nested single-argument functions. Practical benefits: unified partial application syntax (just call with fewer arguments), function composition works naturally on single-argument functions, and point-free style becomes possible. OCaml and Haskell have currying built in; Rust requires explicit nested closures, but the pattern is expressible and useful.
🎯 Learning Outcomes
impl Fn(i32) -> i32 from fn add(x: i32) -> impl Fn(i32) -> i32Box<dyn Fn>curry<F> that converts an uncurried 2-arg function to curried formuncurry that converts curried form back to a 2-arg functionflip that reverses the argument order of a curried functionCode Example
// Explicit currying via nested closures
fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = add(5);
fn curry<A, B, C, F>(f: F) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>Key Differences
add x y is syntactic sugar for fun x -> (fun y -> x + y) β partial application is free. Rust's add(x, y) is a true 2-arg function requiring explicit closure wrapping.Box<dyn Fn> overhead**: Rust's generic curry must use Box<dyn Fn> to erase the return type of the nested closure; OCaml's type system handles this transparently.Copy constraints**: Rust's curry requires A: Copy, B: Copy to capture arguments in nested closures; OCaml copies integers implicitly.flip ergonomics**: Rust's flip requires complex lifetime/trait bounds; OCaml's let flip f x y = f y x is trivial.OCaml Approach
OCaml functions are curried by default:
let add x y = x + y (* add : int -> int -> int β already curried *)
let add5 = add 5 (* partial application β no extra syntax *)
let () = assert (add5 3 = 8)
(* Three-arg currying *)
let clamp lo hi x = max lo (min hi x)
let clamp_0_100 = clamp 0 100
(* Flip *)
let flip f x y = f y x
let sub_from_10 = flip (-) 10 (* fun x -> 10 - x *)
OCaml's default currying makes all of these patterns natural without any boilerplate.
Full Source
#![allow(clippy::all)]
//! Currying Pattern
//!
//! Explicit currying via nested closures returning closures.
/// Curried add: add(x)(y) = x + y
pub fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
/// Curried multiply: mul(x)(y) = x * y
pub fn mul(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
/// Three-argument curried clamp: clamp(lo)(hi)(x)
pub fn clamp(lo: i32) -> Box<dyn Fn(i32) -> Box<dyn Fn(i32) -> i32>> {
Box::new(move |hi| Box::new(move |x| x.max(lo).min(hi)))
}
/// Convert a 2-arg uncurried function to curried form.
pub fn curry<A: Copy + 'static, B: Copy + 'static, C: 'static, F>(
f: F,
) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>
where
F: Fn(A, B) -> C + Copy + 'static,
{
Box::new(move |a| Box::new(move |b| f(a, b)))
}
/// Convert a curried function back to uncurried.
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)
}
/// Flip argument order for a curried function.
pub fn flip<A: 'static, B: Copy + 'static, C: 'static, F, G>(
f: F,
) -> Box<dyn Fn(B) -> Box<dyn Fn(A) -> C>>
where
F: Fn(A) -> G + Clone + 'static,
G: Fn(B) -> C + 'static,
{
Box::new(move |b| {
let f = f.clone();
Box::new(move |a| f(a)(b))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_curried_add() {
assert_eq!(add(5)(3), 8);
let add10 = add(10);
assert_eq!(add10(0), 10);
assert_eq!(add10(5), 15);
}
#[test]
fn test_curried_mul() {
assert_eq!(mul(6)(7), 42);
let times3 = mul(3);
assert_eq!(times3(4), 12);
}
#[test]
fn test_curried_clamp() {
let clamp_5_10 = clamp(5)(10);
assert_eq!(clamp_5_10(7), 7);
assert_eq!(clamp_5_10(2), 5);
assert_eq!(clamp_5_10(15), 10);
}
#[test]
fn test_curry() {
let f = curry(|x: i32, y: i32| x * y);
assert_eq!(f(6)(7), 42);
}
#[test]
fn test_uncurry() {
let g = uncurry(add);
assert_eq!(g(3, 4), 7);
}
#[test]
fn test_partial_application() {
let add5 = add(5);
let result: Vec<i32> = [1, 2, 3, 4, 5].iter().map(|&x| add5(x)).collect();
assert_eq!(result, vec![6, 7, 8, 9, 10]);
}
#[test]
fn test_curried_chain() {
// Chain curried calls
assert_eq!(add(add(1)(2))(3), 6); // (1+2)+3
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_curried_add() {
assert_eq!(add(5)(3), 8);
let add10 = add(10);
assert_eq!(add10(0), 10);
assert_eq!(add10(5), 15);
}
#[test]
fn test_curried_mul() {
assert_eq!(mul(6)(7), 42);
let times3 = mul(3);
assert_eq!(times3(4), 12);
}
#[test]
fn test_curried_clamp() {
let clamp_5_10 = clamp(5)(10);
assert_eq!(clamp_5_10(7), 7);
assert_eq!(clamp_5_10(2), 5);
assert_eq!(clamp_5_10(15), 10);
}
#[test]
fn test_curry() {
let f = curry(|x: i32, y: i32| x * y);
assert_eq!(f(6)(7), 42);
}
#[test]
fn test_uncurry() {
let g = uncurry(add);
assert_eq!(g(3, 4), 7);
}
#[test]
fn test_partial_application() {
let add5 = add(5);
let result: Vec<i32> = [1, 2, 3, 4, 5].iter().map(|&x| add5(x)).collect();
assert_eq!(result, vec![6, 7, 8, 9, 10]);
}
#[test]
fn test_curried_chain() {
// Chain curried calls
assert_eq!(add(add(1)(2))(3), 6); // (1+2)+3
}
}
Deep Comparison
OCaml vs Rust: Currying
OCaml
(* All functions are curried by default *)
let add x y = x + y
let add5 = add 5 (* automatic partial application *)
let curry f x y = f (x, y) (* tuple to curried *)
let uncurry f (x, y) = f x y (* curried to tuple *)
Rust
// Explicit currying via nested closures
fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = add(5);
fn curry<A, B, C, F>(f: F) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>
Key Differences
let f x y = ... is automatically f: a -> b -> cBox<dyn Fn> for nested impl Fn returnsExercises
sum_of_squares: Vec<i32> -> i32 using only map, fold, and curried add/mul β no explicit lambda bodies.curry3 that converts Fn(A, B, C) -> D to Fn(A) -> (Fn(B) -> (Fn(C) -> D)).add, mul, and partial to build a tax_calculator(rate: f64)(price: f64) -> f64 and a discounted_tax(discount: f64)(rate: f64)(price: f64) -> f64.