Closure Partial Application
Tutorial Video
Text description (accessibility)
This video demonstrates the "Closure Partial Application" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. A function `add(x, y)` needs to be passed to `map` which expects `Fn(i32) -> i32`. Key difference from OCaml: 1. **Implicit vs. explicit**: OCaml has built
Tutorial
The Problem
A function add(x, y) needs to be passed to map which expects Fn(i32) -> i32. Partial application solves this: partial(add, 5) returns |y| add(5, y) — the 5 is baked in. This pattern appears everywhere: creating specialised predicates (between(0, 100, x)), building pipeline stages with fixed configuration, and adapting multi-argument functions to single-argument interfaces. Haskell and OCaml have partial application built into the language via currying; Rust requires explicit closures.
🎯 Learning Outcomes
partial that fixes the first argument of a 2-arg functionpartial2 that fixes the first two arguments of a 3-arg functionA: Copy bound — the fixed argument is copied into each returned closurecreate_adder(n) and create_range_checker(lo, hi)Code Example
fn add(x: i32, y: i32) -> i32 { x + y }
let add5 = |y| add(5, y); // explicit closure required
let clamp_0_100 = |x| clamp(0, 100, x);Key Differences
add 5 is legal. Rust requires an explicit closure or partial helper.Copy constraint**: Rust's partial requires A: Copy so the fixed argument is copied into each returned closure; OCaml copies int/float primitives implicitly.partial2 generality**: Rust's partial2 must be defined separately from partial; OCaml's currying handles any number of fixed arguments uniformly.partial is monomorphised — the returned closure is a distinct type per call; OCaml uses uniform representation for all closures.OCaml Approach
OCaml functions are curried by default — partial application is syntactic:
let add x y = x + y
let add5 = add 5 (* partial application — no extra syntax *)
let () = assert (add5 10 = 15)
let clamp lo hi x = max lo (min hi x)
let clamp_to_100 = clamp 0 100 (* fix first two args *)
This is a fundamental difference: OCaml's multi-argument functions are syntactic sugar for nested single-argument functions; Rust's multi-argument functions are genuinely multi-argument.
Full Source
#![allow(clippy::all)]
//! Partial Application with Closures
//!
//! Fix some arguments of a function, producing a specialized version.
/// Add two numbers.
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
/// Clamp value into [lo, hi] range.
pub fn clamp(lo: i32, hi: i32, x: i32) -> i32 {
x.max(lo).min(hi)
}
/// Check if x is in [lo, hi] inclusive.
pub fn between(lo: i32, hi: i32, x: i32) -> bool {
x >= lo && x <= hi
}
/// Generic partial application: fix first argument of a 2-arg function.
pub fn partial<A: Copy, B, C, F>(f: F, a: A) -> impl Fn(B) -> C
where
F: Fn(A, B) -> C,
{
move |b| f(a, b)
}
/// Partial with two fixed args (fix first two of a 3-arg function).
pub fn partial2<A: Copy, B: Copy, C, D, F>(f: F, a: A, b: B) -> impl Fn(C) -> D
where
F: Fn(A, B, C) -> D,
{
move |c| f(a, b, c)
}
/// Manual closure-based partial application approach.
pub fn create_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| add(n, x)
}
/// Create a range checker with fixed bounds.
pub fn create_range_checker(lo: i32, hi: i32) -> impl Fn(i32) -> bool {
move |x| between(lo, hi, x)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manual_partial_add() {
let add5 = |y: i32| add(5, y);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
assert_eq!(add5(-3), 2);
}
#[test]
fn test_manual_partial_clamp() {
let clamp_0_100 = |x| clamp(0, 100, x);
assert_eq!(clamp_0_100(50), 50);
assert_eq!(clamp_0_100(150), 100);
assert_eq!(clamp_0_100(-10), 0);
}
#[test]
fn test_generic_partial() {
let mul_by_7 = partial(|x: i32, y: i32| x * y, 7);
assert_eq!(mul_by_7(6), 42);
assert_eq!(mul_by_7(0), 0);
}
#[test]
fn test_partial2() {
let check = partial2(between, 5, 10);
assert!(check(7));
assert!(check(5));
assert!(check(10));
assert!(!check(4));
assert!(!check(11));
}
#[test]
fn test_partial_string() {
let prefix_checker = partial(|p: &str, s: &str| s.starts_with(p), "rust");
assert!(prefix_checker("rustacean"));
assert!(!prefix_checker("python"));
}
#[test]
fn test_create_adder() {
let add10 = create_adder(10);
assert_eq!(add10(5), 15);
assert_eq!(add10(-10), 0);
}
#[test]
fn test_create_range_checker() {
let in_teens = create_range_checker(13, 19);
assert!(in_teens(15));
assert!(in_teens(13));
assert!(in_teens(19));
assert!(!in_teens(12));
assert!(!in_teens(20));
}
#[test]
fn test_pipeline_with_partial() {
let add5 = |x: &i32| add(5, *x);
let double = |x: i32| x * 2;
let in_teens = |x: &i32| between(13, 19, *x);
let result: Vec<i32> = [1, 2, 3, 4, 5]
.iter()
.map(add5)
.map(double)
.filter(in_teens)
.collect();
assert_eq!(result, vec![14, 16, 18]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manual_partial_add() {
let add5 = |y: i32| add(5, y);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
assert_eq!(add5(-3), 2);
}
#[test]
fn test_manual_partial_clamp() {
let clamp_0_100 = |x| clamp(0, 100, x);
assert_eq!(clamp_0_100(50), 50);
assert_eq!(clamp_0_100(150), 100);
assert_eq!(clamp_0_100(-10), 0);
}
#[test]
fn test_generic_partial() {
let mul_by_7 = partial(|x: i32, y: i32| x * y, 7);
assert_eq!(mul_by_7(6), 42);
assert_eq!(mul_by_7(0), 0);
}
#[test]
fn test_partial2() {
let check = partial2(between, 5, 10);
assert!(check(7));
assert!(check(5));
assert!(check(10));
assert!(!check(4));
assert!(!check(11));
}
#[test]
fn test_partial_string() {
let prefix_checker = partial(|p: &str, s: &str| s.starts_with(p), "rust");
assert!(prefix_checker("rustacean"));
assert!(!prefix_checker("python"));
}
#[test]
fn test_create_adder() {
let add10 = create_adder(10);
assert_eq!(add10(5), 15);
assert_eq!(add10(-10), 0);
}
#[test]
fn test_create_range_checker() {
let in_teens = create_range_checker(13, 19);
assert!(in_teens(15));
assert!(in_teens(13));
assert!(in_teens(19));
assert!(!in_teens(12));
assert!(!in_teens(20));
}
#[test]
fn test_pipeline_with_partial() {
let add5 = |x: &i32| add(5, *x);
let double = |x: i32| x * 2;
let in_teens = |x: &i32| between(13, 19, *x);
let result: Vec<i32> = [1, 2, 3, 4, 5]
.iter()
.map(add5)
.map(double)
.filter(in_teens)
.collect();
assert_eq!(result, vec![14, 16, 18]);
}
}
Deep Comparison
OCaml vs Rust: Partial Application
OCaml
(* Functions are curried by default *)
let add x y = x + y
let add5 = add 5 (* automatic partial application *)
let clamp lo hi x = max lo (min hi x)
let clamp_0_100 = clamp 0 100
Rust
fn add(x: i32, y: i32) -> i32 { x + y }
let add5 = |y| add(5, y); // explicit closure required
let clamp_0_100 = |x| clamp(0, 100, x);
Key Differences
let f = g arg1 creates partiallet f = move |rest| g(arg1, rest) creates partialExercises
partial_right**: Write fn partial_right<A, B: Copy, C, F>(f: F, b: B) -> impl Fn(A) -> C where F: Fn(A, B) -> C that fixes the second argument.partial or manual closures to build get_users, get_orders from a generic make_request(method, endpoint) base function.[partial(clamp, 0), partial(|x,n| x*n, 2), |x| x as f64 / 100.0] using chain_closures from example 505 and verify the output.