Currying, Partial Application, and Operator Sections
Tutorial
The Problem
Demonstrate how OCaml's automatic currying and partial application translate
to Rust's explicit closure-capture model. Show curry/uncurry converters,
flip for argument reordering, operator sections, and function pipelines.
🎯 Learning Outcomes
Box<dyn Fn> is needed when returning closures from generic higher-order functionscurry and uncurry convert between tupled and sequential calling stylesflip (argument-order swap) enables operator sections like halve = flip(div)(2)🦀 The Rust Way
Rust functions take all their arguments at once; partial application is
expressed by returning a closure that captures the fixed arguments. Generic
higher-order functions that return closures (like curry and flip) need
Box<dyn Fn> because Rust cannot yet express impl Fn(A) -> impl Fn(B) -> C
as a stable return type. Function pipelines use .fold() over a slice of
fn pointers, mirroring List.fold_left (fun acc f -> f acc).
Code Example
// Rust functions are NOT curried — partial application via closures
pub fn add(x: i32, y: i32) -> i32 { x + y }
pub fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // closure captures x
}
pub fn double(x: i32) -> i32 { x * 2 }
pub fn increment(x: i32) -> i32 { x + 1 }
pub fn halve(x: i32) -> i32 { x / 2 }
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) // partial application by wrapping
}
// Pipeline: fold a slice of fn pointers
pub fn apply_pipeline(fns: &[fn(i32) -> i32], start: i32) -> i32 {
fns.iter().fold(start, |acc, f| f(acc))
}Key Differences
arguments explicitly with move |y| x + y.
needs Box<dyn Fn> to heap-allocate closures whose concrete type is unknown
at the call site.
~scale and ~shift allow partialapplication in any order; Rust uses positional parameters and wraps calls in closures to fix specific arguments.
( * ) 2 is natural currying; Rust writes |x| x * 2 or defines a named function.
OCaml Approach
In OCaml, every function is curried by default: let add x y = x + y already
has type int -> int -> int, so add 5 is a valid partial application with
type int -> int. Operator sections like ( * ) 2 work because operators are
ordinary curried functions. Fun.flip swaps argument order to enable sections
like halve = Fun.flip ( / ) 2.
Full Source
#![allow(clippy::all)]
//! # Currying, Partial Application, and Operator Sections
//!
//! OCaml functions are curried by default: `add : int -> int -> int` can be
//! partially applied as `add 5 : int -> int`. Rust functions are NOT curried —
//! partial application is achieved explicitly by returning closures.
//!
//! This module shows:
//! 1. Partial application via closures (idiomatic Rust)
//! 2. `curry`/`uncurry` converters between tupled and sequential styles
//! 3. `flip` for swapping argument order (like `Fun.flip`)
//! 4. Operator sections as closures / function definitions
//! 5. Function pipelines via iterator fold
// ---------------------------------------------------------------------------
// Solution 1: Idiomatic Rust — explicit partial application with closures
// ---------------------------------------------------------------------------
/// Binary addition — takes both arguments at once (Rust default style).
pub fn add(x: i32, y: i32) -> i32 {
x + y
}
/// Partial application: fix the first argument, return a closure for the second.
///
/// OCaml equivalent: `let add5 = add 5` (automatic currying)
/// Rust requires an explicit closure that captures `x`.
pub fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
/// Tupled version — takes a pair explicitly.
///
/// Not idiomatic in either OCaml or Rust; shown as the counterpart to `curry`.
pub fn add_tup((x, y): (i32, i32)) -> i32 {
x + y
}
// ---------------------------------------------------------------------------
// Solution 2: curry / uncurry converters (generic, using Box<dyn Fn>)
//
// Rust does not yet stabilise `impl Fn(A) -> impl Fn(B) -> C` as a return
// type, so we return `Box<dyn Fn>` for the inner function. This is the
// idiomatic way to express "higher-order function that returns a function"
// when the concrete closure type cannot be named.
// ---------------------------------------------------------------------------
/// Convert a tupled function into one that accepts arguments one at a time.
///
/// OCaml: `let curry f x y = f (x, y)`
///
/// `A: Clone + 'static` lets the captured `x` be used multiple times inside
/// the heap-allocated inner closure.
pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
F: Fn((A, B)) -> C + Clone + 'static,
A: Clone + 'static,
B: 'static,
C: 'static,
{
move |x: A| {
let f = f.clone();
let x = x.clone();
// Box the inner closure so callers get a consistent dyn Fn type
Box::new(move |y: B| f((x.clone(), y)))
}
}
/// Convert a closure-returning function into one that takes a tuple.
///
/// OCaml: `let uncurry f (x, y) = f x y`
pub fn uncurry<A, B, C, G, F>(f: F) -> impl Fn((A, B)) -> C
where
F: Fn(A) -> G,
G: Fn(B) -> C,
{
// Destructure the tuple argument — idiomatic Rust pattern matching
move |(x, y)| f(x)(y)
}
// ---------------------------------------------------------------------------
// Solution 3: flip — swap argument order (like OCaml's Fun.flip)
// ---------------------------------------------------------------------------
/// Swap the first two arguments of a binary function.
///
/// OCaml: `Fun.flip : ('a -> 'b -> 'c) -> 'b -> 'a -> 'c`
///
/// Usage: `flip(|a, b| a / b)(2)` gives a closure `|x| x / 2`.
pub fn flip<A, B, C, F>(f: F) -> impl Fn(B) -> Box<dyn Fn(A) -> C>
where
F: Fn(A, B) -> C + Clone + 'static,
A: 'static,
B: Clone + 'static,
C: 'static,
{
move |b: B| {
let f = f.clone();
let b = b.clone();
Box::new(move |a: A| f(a, b.clone()))
}
}
// ---------------------------------------------------------------------------
// Operator sections — closures partially applying operators
// ---------------------------------------------------------------------------
/// Equivalent to OCaml's `( * ) 2` — "multiply by 2" section.
pub fn double(x: i32) -> i32 {
x * 2
}
/// Equivalent to OCaml's `( + ) 1` — "add 1" section.
pub fn increment(x: i32) -> i32 {
x + 1
}
/// Equivalent to OCaml's `Fun.flip ( / ) 2` — "divide by 2" section.
/// Integer division, matching OCaml's `/` on integers.
pub fn halve(x: i32) -> i32 {
x / 2
}
// ---------------------------------------------------------------------------
// scale_and_shift — labeled parameters become explicit Rust parameters
// ---------------------------------------------------------------------------
/// General linear transform: `x * scale + shift`.
///
/// OCaml uses labeled arguments (`~scale`, `~shift`) for named partial
/// application in any order. Rust uses positional parameters; partial
/// application wraps the call in a closure.
///
/// OCaml: `let scale_and_shift ~scale ~shift x = x * scale + shift`
pub fn scale_and_shift(scale: i32, shift: i32, x: i32) -> i32 {
x * scale + shift
}
/// Partial application of `scale_and_shift` for Fahrenheit → °C numerator.
///
/// OCaml: `let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)`
///
/// Formula: `F*5 - 160`. Dividing by 9 gives exact integer °C.
/// (32°F → 0, 212°F → 900, 900/9 = 100°C.)
pub fn celsius_of_fahrenheit(fahrenheit: i32) -> i32 {
scale_and_shift(5, -160, fahrenheit)
}
// ---------------------------------------------------------------------------
// Function pipeline via fold (like OCaml's List.fold_left)
// ---------------------------------------------------------------------------
/// Apply a sequence of functions left-to-right, starting from `start`.
///
/// OCaml: `List.fold_left (fun acc f -> f acc) start pipeline`
///
/// Uses `fn(i32) -> i32` pointers so named functions (`double`, etc.) can
/// be stored in a plain slice without boxing.
pub fn apply_pipeline(fns: &[fn(i32) -> i32], start: i32) -> i32 {
fns.iter().fold(start, |acc, f| f(acc))
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partial_application_via_closure() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
// Closure is reusable — callable multiple times
assert_eq!(add5(-3), 2);
// make_adder(0) is the identity function
assert_eq!(make_adder(0)(42), 42);
}
#[test]
fn test_add_and_add_tup_agree() {
assert_eq!(add(3, 4), add_tup((3, 4)));
assert_eq!(add(0, 7), add_tup((0, 7)));
assert_eq!(add(-5, 5), add_tup((-5, 5)));
}
#[test]
fn test_curry_converts_tupled_to_sequential() {
let curried = curry(add_tup);
assert_eq!(curried(3)(4), 7);
assert_eq!(curried(5)(0), 5);
// True partial application: fix first arg, reuse the closure
let add10 = curried(10);
assert_eq!(add10(1), 11);
assert_eq!(add10(90), 100);
}
#[test]
fn test_uncurry_converts_sequential_to_tupled() {
// Pass a closure that returns a closure — the canonical curried style
let tupled = uncurry(|x: i32| move |y: i32| x + y);
assert_eq!(tupled((3, 4)), 7);
assert_eq!(tupled((10, 0)), 10);
assert_eq!(tupled((-1, 1)), 0);
}
#[test]
fn test_flip_swaps_argument_order() {
// subtraction: a - b (non-commutative)
let sub = |a: i32, b: i32| a - b;
let flipped_sub = flip(sub);
// flip(sub)(b)(a) = sub(a, b) = a - b
// flipped_sub(3)(10) = sub(10, 3) = 7
assert_eq!(flipped_sub(3)(10), 7);
// Simulate OCaml's `Fun.flip ( / ) 2` — divide by 2
let halve_fn = flip(|a: i32, b: i32| a / b)(2);
assert_eq!(halve_fn(20), 10);
assert_eq!(halve_fn(7), 3); // integer division truncates
}
#[test]
fn test_operator_sections() {
assert_eq!(double(7), 14);
assert_eq!(double(0), 0);
assert_eq!(increment(41), 42);
assert_eq!(increment(-1), 0);
assert_eq!(halve(20), 10);
assert_eq!(halve(7), 3); // integer division
}
#[test]
fn test_pipeline_fold() {
let pipeline: &[fn(i32) -> i32] = &[double, increment, halve];
// 6 → *2=12 → +1=13 → /2=6
assert_eq!(apply_pipeline(pipeline, 6), 6);
// 10 → *2=20 → +1=21 → /2=10
assert_eq!(apply_pipeline(pipeline, 10), 10);
// Empty pipeline is identity
assert_eq!(apply_pipeline(&[], 42), 42);
}
#[test]
fn test_celsius_formula_boundary_values() {
// 32°F = freezing: 32*5 - 160 = 0 (0/9 = 0°C)
assert_eq!(celsius_of_fahrenheit(32), 0);
// 212°F = boiling: 212*5 - 160 = 900 (900/9 = 100°C)
assert_eq!(celsius_of_fahrenheit(212), 900);
// scale_and_shift is a general linear transform
assert_eq!(scale_and_shift(2, 3, 5), 13); // 5*2+3=13
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partial_application_via_closure() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
// Closure is reusable — callable multiple times
assert_eq!(add5(-3), 2);
// make_adder(0) is the identity function
assert_eq!(make_adder(0)(42), 42);
}
#[test]
fn test_add_and_add_tup_agree() {
assert_eq!(add(3, 4), add_tup((3, 4)));
assert_eq!(add(0, 7), add_tup((0, 7)));
assert_eq!(add(-5, 5), add_tup((-5, 5)));
}
#[test]
fn test_curry_converts_tupled_to_sequential() {
let curried = curry(add_tup);
assert_eq!(curried(3)(4), 7);
assert_eq!(curried(5)(0), 5);
// True partial application: fix first arg, reuse the closure
let add10 = curried(10);
assert_eq!(add10(1), 11);
assert_eq!(add10(90), 100);
}
#[test]
fn test_uncurry_converts_sequential_to_tupled() {
// Pass a closure that returns a closure — the canonical curried style
let tupled = uncurry(|x: i32| move |y: i32| x + y);
assert_eq!(tupled((3, 4)), 7);
assert_eq!(tupled((10, 0)), 10);
assert_eq!(tupled((-1, 1)), 0);
}
#[test]
fn test_flip_swaps_argument_order() {
// subtraction: a - b (non-commutative)
let sub = |a: i32, b: i32| a - b;
let flipped_sub = flip(sub);
// flip(sub)(b)(a) = sub(a, b) = a - b
// flipped_sub(3)(10) = sub(10, 3) = 7
assert_eq!(flipped_sub(3)(10), 7);
// Simulate OCaml's `Fun.flip ( / ) 2` — divide by 2
let halve_fn = flip(|a: i32, b: i32| a / b)(2);
assert_eq!(halve_fn(20), 10);
assert_eq!(halve_fn(7), 3); // integer division truncates
}
#[test]
fn test_operator_sections() {
assert_eq!(double(7), 14);
assert_eq!(double(0), 0);
assert_eq!(increment(41), 42);
assert_eq!(increment(-1), 0);
assert_eq!(halve(20), 10);
assert_eq!(halve(7), 3); // integer division
}
#[test]
fn test_pipeline_fold() {
let pipeline: &[fn(i32) -> i32] = &[double, increment, halve];
// 6 → *2=12 → +1=13 → /2=6
assert_eq!(apply_pipeline(pipeline, 6), 6);
// 10 → *2=20 → +1=21 → /2=10
assert_eq!(apply_pipeline(pipeline, 10), 10);
// Empty pipeline is identity
assert_eq!(apply_pipeline(&[], 42), 42);
}
#[test]
fn test_celsius_formula_boundary_values() {
// 32°F = freezing: 32*5 - 160 = 0 (0/9 = 0°C)
assert_eq!(celsius_of_fahrenheit(32), 0);
// 212°F = boiling: 212*5 - 160 = 900 (900/9 = 100°C)
assert_eq!(celsius_of_fahrenheit(212), 900);
// scale_and_shift is a general linear transform
assert_eq!(scale_and_shift(2, 3, 5), 13); // 5*2+3=13
}
}
Deep Comparison
OCaml vs Rust: Currying, Partial Application, and Operator Sections
Side-by-Side Code
OCaml
(* All OCaml functions are curried by default *)
let add x y = x + y
let add5 = add 5 (* partial application, no extra syntax *)
let add_tup (x, y) = x + y (* tupled — NOT the default *)
let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
(* Operator sections *)
let double = ( * ) 2
let increment = ( + ) 1
let halve = Fun.flip ( / ) 2
(* Labeled args: partial application in any order *)
let scale_and_shift ~scale ~shift x = x * scale + shift
let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)
Rust (idiomatic)
// Rust functions are NOT curried — partial application via closures
pub fn add(x: i32, y: i32) -> i32 { x + y }
pub fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // closure captures x
}
pub fn double(x: i32) -> i32 { x * 2 }
pub fn increment(x: i32) -> i32 { x + 1 }
pub fn halve(x: i32) -> i32 { x / 2 }
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) // partial application by wrapping
}
// Pipeline: fold a slice of fn pointers
pub fn apply_pipeline(fns: &[fn(i32) -> i32], start: i32) -> i32 {
fns.iter().fold(start, |acc, f| f(acc))
}
Rust (functional — curry/uncurry/flip with Box<dyn Fn>)
// curry: tupled function → sequential (one arg at a time)
pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
F: Fn((A, B)) -> C + Clone + 'static,
A: Clone + 'static,
B: 'static,
C: 'static,
{
move |x: A| {
let f = f.clone();
let x = x.clone();
Box::new(move |y: B| f((x.clone(), y)))
}
}
// uncurry: closure-returning function → tupled
pub fn uncurry<A, B, C, G, F>(f: F) -> impl Fn((A, B)) -> C
where
F: Fn(A) -> G,
G: Fn(B) -> C,
{
move |(x, y)| f(x)(y)
}
// flip: swap first two arguments (like OCaml's Fun.flip)
pub fn flip<A, B, C, F>(f: F) -> impl Fn(B) -> Box<dyn Fn(A) -> C>
where
F: Fn(A, B) -> C + Clone + 'static,
A: 'static,
B: Clone + 'static,
C: 'static,
{
move |b: B| {
let f = f.clone();
let b = b.clone();
Box::new(move |a: A| f(a, b.clone()))
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Binary add | val add : int -> int -> int | fn add(x: i32, y: i32) -> i32 |
| Partial add | val add5 : int -> int | let add5: impl Fn(i32) -> i32 = make_adder(5) |
| Tupled add | val add_tup : int * int -> int | fn add_tup((x,y): (i32,i32)) -> i32 |
| curry | val curry : ('a*'b->'c) -> 'a -> 'b -> 'c | fn curry<A,B,C,F>(f:F) -> impl Fn(A) -> Box<dyn Fn(B)->C> |
| uncurry | val uncurry : ('a->'b->'c) -> 'a*'b -> 'c | fn uncurry<A,B,C,G,F>(f:F) -> impl Fn((A,B))->C |
| flip | val flip : ('a->'b->'c) -> 'b -> 'a -> 'c | fn flip<A,B,C,F>(f:F) -> impl Fn(B) -> Box<dyn Fn(A)->C> |
| Operator section | ( * ) 2 : int -> int | \|x\| x * 2 or fn double(x:i32)->i32 |
Key Insights
can be partially applied with no extra syntax. In Rust you must explicitly
return a closure (move |y| x + y) that captures the fixed argument.
Box<dyn Fn> for higher-order generics.** When writing generic curry or flip, the inner closure has an un-nameable concrete type. Returning
Box<dyn Fn(B) -> C> heap-allocates it and erases the type — the cost of
Rust's zero-overhead abstractions at this boundary. (A nightly feature
impl_trait_in_fn_trait_return will eventually remove this need.)
curry requires A: Clone + 'static and F: Clone + 'static because the inner Box must
own its captures and those captures may be used repeatedly. OCaml's GC
handles this transparently.
~scale and ~shift allow calling scale_and_shift ~shift:(-160) ~scale:5 in any
order. Rust requires a wrapper closure to fix specific arguments of a
positional function.
[double; increment; halve] stores closures of the same type naturally. In Rust, a &[fn(i32)->i32]
slice of function pointers works when all functions are named (not closures
with captured state); mixed cases require Vec<Box<dyn Fn(i32)->i32>>.
When to Use Each Style
**Use impl Fn return (make_adder style) when:** creating a simple partially-
applied function where the captured type is known and the return doesn't need
to be stored generically.
**Use Box<dyn Fn> when:** writing generic higher-order combinators (curry,
flip) where the inner closure type cannot be named, or storing mixed closures
in a collection.
Exercises
zip_with: (A -> B -> C) -> [A] -> [B] -> [C] and use partial application to create a vector addition function from a curried add.uncurry that converts a curried function A -> B -> C back into a two-argument function (A, B) -> C.eval_binop parameterized by operator and operands, composed with a parser that splits an infix expression string.