024: Currying, Partial Application, and Operator Sections
Tutorial
The Problem
Show how OCaml's automatic currying, partial application, operator sections, and labeled arguments translate to idiomatic Rust using closures and higher-order functions.
🎯 Learning Outcomes
impl Fn(T) -> U is the return type for a partially-applied functionmultiply(2) instead of ( * ) 2)~scale ~shift) become Rust curried parameter orderBox<dyn Fn> is needed when the inner closure type must be named (e.g., in curry/uncurry)🦀 The Rust Way
Rust functions are not automatically curried. To get add(5) as a value of type
impl Fn(i32) -> i32, we return a capturing closure: move |y| x + y. The
move keyword transfers ownership of x into the closure. For generic
curry/uncurry conversions, Box<dyn Fn(B) -> C> stands in for the unnamed
inner closure type that OCaml expresses transparently.
Code Example
// Rust functions are NOT curried — return a closure explicitly
fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = add(5); // add5: impl Fn(i32) -> i32
// Tupled add — identical concept, just tuple destructuring in the arg
fn add_tupled((x, y): (i32, i32)) -> i32 { x + y }
// Operator section factories
fn multiply(n: i32) -> impl Fn(i32) -> i32 { move |x| x * n }
fn divide_by(n: i32) -> impl Fn(i32) -> i32 { move |x| x / n }
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2); // Fun.flip ( / ) 2 — arg order explicit
// Labeled-arg partial application: fixed order, same result
fn scale_and_shift(scale: i32, shift: i32) -> impl Fn(i32) -> i32 {
move |x| x * scale + shift
}
let celsius_of_fahrenheit = scale_and_shift(5, -160);Key Differences
( * ) 2; Rust writes move |x| x * 2 or a factory.Fun.flip:** OCaml flips argument order with a combinator; Rust just writes the closure directly.~scale ~shift allows out-of-order partial application; Rust fixes the order and curries sequentially.impl Fn vs Box<dyn Fn>:** Rust uses impl Fn for simple cases and Box<dyn Fn> when the type must be stored or returned through a trait object boundary.OCaml Approach
OCaml curries every function by default: let add x y = x + y is actually
fun x -> fun y -> x + y. This means add 5 is already a value of type
int -> int — no extra syntax required. Operator sections like ( * ) 2 and
Fun.flip ( / ) 2 exploit this to build predicates and transformers inline.
Full Source
#![allow(dead_code)]
//! Currying, partial application, and operator sections in Rust vs OCaml.
//!
//! OCaml curries ALL functions by default — `let add x y = x + y` is sugar
//! for `fun x -> fun y -> x + y`. Rust requires explicit closures, but reaches
//! the same expressiveness. This module shows every pattern side-by-side.
// ── 1. Curried add: direct OCaml parallel ──────────────────────────────────
//
// OCaml: let add x y = x + y
// let add5 = add 5
//
// Rust cannot auto-curry, so we return a closure explicitly.
/// Curried add: `add(5)` returns a function that adds 5 to any i32.
pub fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// ── 2. Tupled style ────────────────────────────────────────────────────────
//
// OCaml: let add_tup (x, y) = x + y
//
// OCaml tupled functions are NOT curried; Rust tuple-arg functions are the
// same — one call, no partial application.
/// Tupled add: takes `(i32, i32)` as a single argument.
pub fn add_tupled((x, y): (i32, i32)) -> i32 {
x + y
}
// ── 3. curry / uncurry ─────────────────────────────────────────────────────
//
// OCaml: let curry f x y = f (x, y) (* tupled -> curried *)
// let uncurry f (x, y) = f x y (* curried -> tupled *)
//
// Rust can't express `impl Fn(A) -> impl Fn(B) -> C` in stable syntax, so we
// use `Box<dyn Fn>` for the inner step. Function items implement `Copy`, which
// lets us avoid Arc/Rc for the common case.
/// Convert a tupled function into a curried form.
///
/// Returns a closure `A -> Box<dyn Fn(B) -> C>`.
/// The `Box` is needed because stable Rust can't name the inner closure type.
pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
F: Fn((A, B)) -> C + Copy + 'static,
A: Copy + 'static,
B: 'static,
C: 'static,
{
move |x: A| Box::new(move |y: B| f((x, y)))
}
/// Convert a curried function (returning `Box<dyn Fn>`) into a tupled form.
///
/// Works with the output of [`curry`].
/// OCaml: `let uncurry f (x, y) = f x y`
pub fn uncurry<A, B, C>(f: impl Fn(A) -> Box<dyn Fn(B) -> C> + 'static) -> impl Fn((A, B)) -> C {
move |(x, y)| f(x)(y)
}
// ── 4. Operator sections ────────────────────────────────────────────────────
//
// OCaml: let double = ( * ) 2 (* section: fix left arg of * *)
// let increment = ( + ) 1
// let halve = Fun.flip ( / ) 2 (* flip fixes RIGHT arg *)
//
// Rust: partial application via closures. `Fun.flip` is just argument reorder.
/// Multiply-by-n factory — equivalent to OCaml's `( * ) n` operator section.
pub fn multiply(n: i32) -> impl Fn(i32) -> i32 {
move |x| x * n
}
/// Divide-into-n — equivalent to OCaml's `Fun.flip ( / ) n`.
///
/// `divide_by(2)(20) == 10` (divides the *argument* by n, not n by argument)
pub fn divide_by(n: i32) -> impl Fn(i32) -> i32 {
move |x| x / n
}
// Usage:
// let double = multiply(2);
// let increment = add(1);
// let halve = divide_by(2);
// ── 5. Labeled arguments → curried factories ───────────────────────────────
//
// OCaml has labeled `~param` syntax so you can partially apply 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 has no labeled args. We emulate ordered partial application instead.
/// Curried scale-and-shift: fixes `scale` and `shift`, returns a transformer.
///
/// OCaml: `let scale_and_shift ~scale ~shift x = x * scale + shift`
pub fn scale_and_shift(scale: i32, shift: i32) -> impl Fn(i32) -> i32 {
move |x| x * scale + shift
}
/// Fahrenheit → approximate Celsius using `scale_and_shift`.
///
/// OCaml: `let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)`
///
/// Note: integer approximation — `(32°F) * 5 - 160 = 0`, `(212°F) * 5 - 160 = 900`.
/// The exact formula is `(F - 32) * 5 / 9`; this example shows the *pattern*,
/// not a production-quality converter.
pub fn celsius_of_fahrenheit() -> impl Fn(i32) -> i32 {
scale_and_shift(5, -160)
}
// ── 6. Pipeline via fold ────────────────────────────────────────────────────
//
// OCaml: let pipeline = [double; increment; halve]
// let result = List.fold_left (fun acc f -> f acc) 6 pipeline
//
// OCaml's homogeneous function list maps to `&[&dyn Fn(i32) -> i32]` in Rust.
/// Apply a pipeline of transformations via left-fold.
///
/// Mirrors OCaml's `List.fold_left (fun acc f -> f acc) initial pipeline`.
pub fn pipeline(initial: i32, transforms: &[&dyn Fn(i32) -> i32]) -> i32 {
transforms.iter().fold(initial, |acc, f| f(acc))
}
#[cfg(test)]
mod tests {
use super::*;
// ── add / partial application ──────────────────────────────────────────
#[test]
fn test_partial_application_add5() {
let add5 = add(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_add_tupled() {
assert_eq!(add_tupled((3, 4)), 7);
assert_eq!(add_tupled((0, 0)), 0);
assert_eq!(add_tupled((-1, 1)), 0);
assert_eq!(add_tupled((-3, -4)), -7);
}
// ── curry / uncurry ───────────────────────────────────────────────────
#[test]
fn test_curry_converts_tupled_to_curried() {
let curried = curry(add_tupled);
assert_eq!(curried(3)(4), 7);
assert_eq!(curried(0)(0), 0);
assert_eq!(curried(-1)(1), 0);
}
#[test]
fn test_uncurry_converts_back_to_tupled() {
let tupled = uncurry(curry(add_tupled));
assert_eq!(tupled((3, 4)), 7);
assert_eq!(tupled((0, 0)), 0);
assert_eq!(tupled((-5, 5)), 0);
assert_eq!(tupled((10, 20)), 30);
}
// ── operator sections ─────────────────────────────────────────────────
#[test]
fn test_operator_sections_double_increment_halve() {
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2);
assert_eq!(double(7), 14);
assert_eq!(increment(9), 10);
assert_eq!(halve(20), 10);
assert_eq!(halve(21), 10); // integer division truncates
}
// ── pipeline ──────────────────────────────────────────────────────────
#[test]
fn test_pipeline_fold() {
// OCaml: 6 |> double |> increment |> halve = (6*2+1)/2 = 13/2 = 6
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2);
let result = pipeline(6, &[&double, &increment, &halve]);
assert_eq!(result, 6); // 6→12→13→6 (integer division)
}
#[test]
fn test_pipeline_empty() {
assert_eq!(pipeline(42, &[]), 42);
}
// ── scale_and_shift / labeled args ────────────────────────────────────
#[test]
fn test_celsius_of_fahrenheit_fixed_points() {
let to_c = celsius_of_fahrenheit();
// 32°F → 32*5-160 = 0 (water freezing — correct!)
assert_eq!(to_c(32), 0);
// 212°F → 212*5-160 = 900 (integer approximation — pattern demo)
assert_eq!(to_c(212), 900);
}
#[test]
fn test_scale_and_shift_generic() {
// identity: scale=1, shift=0
let id = scale_and_shift(1, 0);
assert_eq!(id(42), 42);
// double-and-add-three
let f = scale_and_shift(2, 3);
assert_eq!(f(5), 13); // 5*2+3
assert_eq!(f(0), 3);
}
}#[cfg(test)]
mod tests {
use super::*;
// ── add / partial application ──────────────────────────────────────────
#[test]
fn test_partial_application_add5() {
let add5 = add(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
assert_eq!(add5(-5), 0);
}
#[test]
fn test_add_tupled() {
assert_eq!(add_tupled((3, 4)), 7);
assert_eq!(add_tupled((0, 0)), 0);
assert_eq!(add_tupled((-1, 1)), 0);
assert_eq!(add_tupled((-3, -4)), -7);
}
// ── curry / uncurry ───────────────────────────────────────────────────
#[test]
fn test_curry_converts_tupled_to_curried() {
let curried = curry(add_tupled);
assert_eq!(curried(3)(4), 7);
assert_eq!(curried(0)(0), 0);
assert_eq!(curried(-1)(1), 0);
}
#[test]
fn test_uncurry_converts_back_to_tupled() {
let tupled = uncurry(curry(add_tupled));
assert_eq!(tupled((3, 4)), 7);
assert_eq!(tupled((0, 0)), 0);
assert_eq!(tupled((-5, 5)), 0);
assert_eq!(tupled((10, 20)), 30);
}
// ── operator sections ─────────────────────────────────────────────────
#[test]
fn test_operator_sections_double_increment_halve() {
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2);
assert_eq!(double(7), 14);
assert_eq!(increment(9), 10);
assert_eq!(halve(20), 10);
assert_eq!(halve(21), 10); // integer division truncates
}
// ── pipeline ──────────────────────────────────────────────────────────
#[test]
fn test_pipeline_fold() {
// OCaml: 6 |> double |> increment |> halve = (6*2+1)/2 = 13/2 = 6
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2);
let result = pipeline(6, &[&double, &increment, &halve]);
assert_eq!(result, 6); // 6→12→13→6 (integer division)
}
#[test]
fn test_pipeline_empty() {
assert_eq!(pipeline(42, &[]), 42);
}
// ── scale_and_shift / labeled args ────────────────────────────────────
#[test]
fn test_celsius_of_fahrenheit_fixed_points() {
let to_c = celsius_of_fahrenheit();
// 32°F → 32*5-160 = 0 (water freezing — correct!)
assert_eq!(to_c(32), 0);
// 212°F → 212*5-160 = 900 (integer approximation — pattern demo)
assert_eq!(to_c(212), 900);
}
#[test]
fn test_scale_and_shift_generic() {
// identity: scale=1, shift=0
let id = scale_and_shift(1, 0);
assert_eq!(id(42), 42);
// double-and-add-three
let f = scale_and_shift(2, 3);
assert_eq!(f(5), 13); // 5*2+3
assert_eq!(f(0), 3);
}
}
Deep Comparison
OCaml vs Rust: Currying, Partial Application, and Operator Sections
Side-by-Side Code
OCaml
(* Every OCaml function is automatically curried *)
let add x y = x + y
let add5 = add 5 (* partial application — zero syntax *)
(* Tupled style requires explicit destructuring *)
let add_tup (x, y) = x + y
(* curry / uncurry bridge the two styles *)
let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
(* Operator sections fix one operand of an infix operator *)
let double = ( * ) 2
let increment = ( + ) 1
let halve = Fun.flip ( / ) 2 (* flip swaps args: halve x = x / 2 *)
(* Labeled args allow 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 — return a closure explicitly
fn add(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = add(5); // add5: impl Fn(i32) -> i32
// Tupled add — identical concept, just tuple destructuring in the arg
fn add_tupled((x, y): (i32, i32)) -> i32 { x + y }
// Operator section factories
fn multiply(n: i32) -> impl Fn(i32) -> i32 { move |x| x * n }
fn divide_by(n: i32) -> impl Fn(i32) -> i32 { move |x| x / n }
let double = multiply(2);
let increment = add(1);
let halve = divide_by(2); // Fun.flip ( / ) 2 — arg order explicit
// Labeled-arg partial application: fixed order, same result
fn scale_and_shift(scale: i32, shift: i32) -> impl Fn(i32) -> i32 {
move |x| x * scale + shift
}
let celsius_of_fahrenheit = scale_and_shift(5, -160);
Rust (generic curry / uncurry)
// curry: tupled fn → curried fn
// Box<dyn Fn> needed because stable Rust can't write `impl Fn(A) -> impl Fn(B) -> C`
fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
F: Fn((A, B)) -> C + Copy + 'static,
A: Copy + 'static,
B: 'static,
C: 'static,
{
move |x: A| Box::new(move |y: B| f((x, y)))
}
// uncurry: curried fn → tupled fn
fn uncurry<A, B, C>(
f: impl Fn(A) -> Box<dyn Fn(B) -> C> + 'static,
) -> impl Fn((A, B)) -> C {
move |(x, y)| f(x)(y)
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Curried add | val add : int -> int -> int | fn add(x: i32) -> impl Fn(i32) -> i32 |
| Partial application | let add5 = add 5 (type: int -> int) | let add5 = add(5) (type: impl Fn(i32) -> i32) |
| Tupled add | val add_tup : int * int -> int | fn add_tupled((x,y): (i32,i32)) -> i32 |
| Operator section | ( * ) 2 : int -> int | multiply(2) returns impl Fn(i32) -> i32 |
| Flip | Fun.flip ( / ) 2 : int -> int | divide_by(2) — closure order explicit |
| Generic 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> |
| Generic uncurry | val uncurry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c | fn uncurry<A,B,C>(f: impl Fn(A)->Box<dyn Fn(B)->C>+'static) -> impl Fn((A,B))->C |
Key Insights
fn f(x: T) -> impl Fn(U) -> V and return a move closure. The intent is just as clear, but the annotation cost is higher.impl Fn vs Box<dyn Fn>:** impl Fn(T) -> U works perfectly when the compiler can infer the concrete closure type at the call site. Once the type must cross a boundary — stored in a struct, returned through a generic function, or used as a trait object — Box<dyn Fn(T) -> U> is the idiomatic choice. OCaml never needs this distinction.fn curry<A,B,C,F>(f:F) -> impl Fn(A) -> impl Fn(B) -> C on stable (issue #99697). The inner impl Fn inside a Fn bound is not yet supported. Box<dyn Fn> is the stable workaround; F: Copy lets us avoid Arc/Rc for function items.Fun.flip vs closure argument reorder:** OCaml has a higher-order flip combinator. In Rust, we simply capture the desired operand directly: divide_by(n) captures n as the divisor and accepts the dividend as the closure arg. No flip is needed — the closure naturally expresses the argument order.~label syntax lets you apply any subset of arguments in any order. Rust has no equivalent. The idiom is to curry in a fixed order, or use a builder struct when argument naming is critical.When to Use Each Style
**Use impl Fn return:** When writing a simple factory or partial application that stays within a single function's scope and the concrete type doesn't need to be stored.
**Use Box<dyn Fn> return:** When the closure must be stored in a Vec, returned from a generic function, or composed through multiple layers where the concrete type is unknown.
**Use a struct with Fn bound:** When partial application involves many parameters, named fields improve clarity (analogous to OCaml labeled args).