074 — Currying and Partial Application (Applied)
Tutorial
The Problem
This example applies currying and partial application to practical patterns — building factories, greeting generators, and transformation pipelines. Where example 005 introduced the theory, this example shows the patterns in context: make_adder(5) returns a reusable function, apply_twice demonstrates function composition from first principles.
Partial application is ubiquitous in functional programming: event handler factories in UIs, middleware pipelines in web frameworks, predicate factories in query builders, and configuration-bound operations. Understanding how to build and compose these in Rust is essential for ergonomic API design.
🎯 Learning Outcomes
make_adder(n) -> impl Fn(i32) -> i32apply_twice to demonstrate functions as valuescompose to build pipelinesmove keyword for capturing environment in closuresCode Example
#![allow(clippy::all)]
// 074: Currying and Partial Application
// Approach 1: Closures for partial application
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
// Approach 2: Curried function returning closures
fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String + '_> + '_ {
move |name: &str| {
let owned_prefix = prefix.to_string();
let owned_name = name.to_string();
Box::new(move |suffix: &str| format!("{} {}{}", owned_prefix, owned_name, suffix))
}
}
// Simpler version:
fn greet(prefix: &str, name: &str, suffix: &str) -> String {
format!("{} {}{}", prefix, name, suffix)
}
// Approach 3: Higher-order + partial application
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
move |x| f(g(x))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partial_application() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
let double = make_multiplier(2);
assert_eq!(double(7), 14);
let triple = make_multiplier(3);
assert_eq!(triple(4), 12);
}
#[test]
fn test_apply_twice() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn test_compose() {
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3+5)*2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3*2+5
}
#[test]
fn test_greet() {
assert_eq!(greet("Hello", "World", "!"), "Hello World!");
}
}Key Differences
move requirement**: Rust requires move in the closure to capture x by value. OCaml captures by closure environment automatically (GC-managed). Without move, Rust would borrow x — problematic when the closure outlives the function.impl Fn return type**: Rust's -> impl Fn(i32) -> i32 return type is required because closures have unique anonymous types. OCaml's fun x -> ... return type is inferred as int -> int.move), so it has 'static lifetime. OCaml closures can safely reference the outer scope due to GC.compose(f, g) in Rust requires + 'static bounds if stored. OCaml's fun x -> f (g x) composes naturally.OCaml Approach
OCaml functions are automatically curried, so partial application is natural:
let make_adder x y = x + y (* equivalent to: let make_adder x = fun y -> x + y *)
let add5 = make_adder 5 (* partial application: bind x=5 *)
let _ = add5 3 (* evaluates to 8 *)
let apply_twice f x = f (f x)
let compose f g = fun x -> f (g x)
let double_then_add5 = compose add5 (fun x -> x * 2)
OCaml's implicit currying means every multi-argument function is already a curried function. Partial application is just function application with fewer arguments than the full arity.
Full Source
#![allow(clippy::all)]
// 074: Currying and Partial Application
// Approach 1: Closures for partial application
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
// Approach 2: Curried function returning closures
fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String + '_> + '_ {
move |name: &str| {
let owned_prefix = prefix.to_string();
let owned_name = name.to_string();
Box::new(move |suffix: &str| format!("{} {}{}", owned_prefix, owned_name, suffix))
}
}
// Simpler version:
fn greet(prefix: &str, name: &str, suffix: &str) -> String {
format!("{} {}{}", prefix, name, suffix)
}
// Approach 3: Higher-order + partial application
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
move |x| f(g(x))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partial_application() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
let double = make_multiplier(2);
assert_eq!(double(7), 14);
let triple = make_multiplier(3);
assert_eq!(triple(4), 12);
}
#[test]
fn test_apply_twice() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn test_compose() {
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3+5)*2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3*2+5
}
#[test]
fn test_greet() {
assert_eq!(greet("Hello", "World", "!"), "Hello World!");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partial_application() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
assert_eq!(add5(0), 5);
let double = make_multiplier(2);
assert_eq!(double(7), 14);
let triple = make_multiplier(3);
assert_eq!(triple(4), 12);
}
#[test]
fn test_apply_twice() {
let add5 = make_adder(5);
assert_eq!(apply_twice(&add5, 0), 10);
assert_eq!(apply_twice(&add5, 5), 15);
let double = make_multiplier(2);
assert_eq!(apply_twice(&double, 3), 12);
}
#[test]
fn test_compose() {
let add5_then_double = compose(make_multiplier(2), make_adder(5));
assert_eq!(add5_then_double(3), 16); // (3+5)*2
let double_then_add5 = compose(make_adder(5), make_multiplier(2));
assert_eq!(double_then_add5(3), 11); // 3*2+5
}
#[test]
fn test_greet() {
assert_eq!(greet("Hello", "World", "!"), "Hello World!");
}
}
Deep Comparison
Core Insight
In OCaml, let add x y = x + y is actually let add = fun x -> fun y -> x + y. Partial application (add 5) returns a function. Rust functions aren't curried — you use closures to simulate partial application.
OCaml Approach
let add x y = x + y → add 5 returns fun y -> 5 + yRust Approach
let add5 = |y| 5 + y;move closures for ownership transferimpl Fn(T) -> UComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Curried by default | Yes | No |
| Partial application | f x (give fewer args) | Closure capturing |
| Return function | Automatic | impl Fn(T) -> U |
| Closure syntax | fun x -> ... | \|x\| ... |
Exercises
make_tax_calculator(rate: f64) -> impl Fn(f64) -> f64 and make_discount(pct: f64) -> impl Fn(f64) -> f64. Compose them into a calculate_price pipeline.memoized_make_adder(cache: &mut HashMap<i32, Box<dyn Fn(i32) -> i32>>, n: i32) -> &Box<dyn Fn(i32) -> i32> that caches the created adder function.compose, build a pipeline for data transformation: trim -> lowercase -> split_words -> filter_short_words -> join_with_comma. Each step is a partial application of a generic combinator.