๐Ÿฆ€ Functional Rust

074: Currying and Partial Application

Difficulty: 2 Level: Beginner OCaml curries everything automatically. Rust doesn't โ€” but closures capture arguments explicitly with equal power and more transparency.

The Problem This Solves

You want to reuse a multi-argument function with one argument fixed: an adder with a fixed offset, a comparator with a fixed threshold, a formatter with a fixed template. In a higher-order pipeline, you need to produce a single-argument function from a multi-argument one. In OCaml this is free: `let add5 = add 5` partially applies `add` instantly. In Rust you write `let add5 = |y| add(5, y)` โ€” explicit, but it makes the capture visible. The power is the same; the ceremony differs.

The Intuition

Currying means every function takes exactly one argument and returns either a value or another function. `add 5 3` is really `(add 5) 3` โ€” apply `add` to `5`, get a new function, apply that to `3`. Rust's closures capture bindings explicitly: `move |y| x + y` captures `x` by value and takes `y` as argument. This is manual currying โ€” more explicit but also clearer about what is captured. The `move` keyword is the Rust equivalent of OCaml's value capture โ€” it ensures the closure owns its captured variables and can outlive the scope that created them.

How It Works in Rust

// Regular two-argument function โ€” NOT curried
pub fn add(x: i32, y: i32) -> i32 { x + y }

// Partial application via closure โ€” the idiomatic Rust way
pub fn add5() -> impl Fn(i32) -> i32 {
 move |y| add(5, y)   // captures 5, takes y as argument
}

// Curried form: takes x, returns a function that takes y
pub fn add_curried(x: i32) -> impl Fn(i32) -> i32 {
 move |y| x + y   // `move` captures x by value into the closure
}

// Operator sections via closures
pub fn double()    -> impl Fn(i32) -> i32 { |x| x * 2 }
pub fn increment() -> impl Fn(i32) -> i32 { |x| x + 1 }

// Use them in a pipeline
fn pipeline(data: &[i32]) -> Vec<i32> {
 data.iter()
     .copied()
     .map(add_curried(10))  // partial: add 10 to each
     .map(double())          // double each result
     .collect()
}

// Generic curry converter: (A, B) -> C becomes A -> (B -> C)
pub fn curry<A, B, C>(f: impl Fn(A, B) -> C + 'static) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where A: Copy + 'static, B: 'static, C: 'static {
 move |a| Box::new(move |b| f(a, b))
}

// Compose two functions: g(f(x))
pub fn compose<A, B, C>(
 f: impl Fn(A) -> B,
 g: impl Fn(B) -> C,
) -> impl Fn(A) -> C {
 move |x| g(f(x))
}
Note: returning `impl Fn(i32) -> i32` works for simple cases. For storing in a struct or returning from a trait object, use `Box<dyn Fn(i32) -> i32>`.

What This Unlocks

Key Differences

ConceptOCamlRust
Default curryingAutomatic โ€” every functionNot automatic โ€” must use closures
Partial apply`let add5 = add 5``let add5 = \y\add(5, y)`
CaptureImplicit (lexical scope)Explicit `move` for owned capture
Compose`\>` pipe or `Fun.compose`Custom `compose()` or `\>` macro
Return type`int -> int``impl Fn(i32) -> i32` or `Box<dyn Fn>`
/// Currying, Partial Application, and Sections
///
/// OCaml functions are curried by default: `let add x y = x + y` can be
/// partially applied as `add 5`. Rust functions are NOT curried โ€” closures
/// are used instead for partial application.

/// Regular two-argument function (NOT curried in Rust).
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

/// Partial application via closure โ€” the Rust way.
pub fn add5() -> impl Fn(i32) -> i32 {
    move |y| add(5, y)
}

/// Curried function โ€” returns a closure. This mimics OCaml's default.
pub fn add_curried(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

/// Operator "sections" via closures.
pub fn double() -> impl Fn(i32) -> i32 {
    |x| x * 2
}

pub fn increment() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

pub fn halve() -> impl Fn(i32) -> i32 {
    |x| x / 2
}

/// Curry converter: turns a 2-arg function into a curried one.
/// Requires A: Copy so the closure can capture it by value in Fn.
pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
where
    A: Copy + 'static,
    B: 'static,
    C: 'static,
    F: Fn(A, B) -> C + Clone + 'static,
{
    move |a: A| {
        let f = f.clone();
        Box::new(move |b: B| f(a, b))
    }
}

/// Uncurry converter: turns a curried function into a 2-arg one.
pub fn uncurry<A, B, C>(f: impl Fn(A) -> Box<dyn Fn(B) -> C>) -> impl Fn(A, B) -> C {
    move |a, b| f(a)(b)
}

/// Pipeline: fold a value through a list of functions.
pub fn pipeline(initial: i32, funcs: &[&dyn Fn(i32) -> i32]) -> i32 {
    funcs.iter().fold(initial, |acc, f| f(acc))
}

/// Scale and shift with named parameters (Rust doesn't have labeled args,
/// but builder pattern or structs serve the same purpose).
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)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add5() {
        assert_eq!(add5()(10), 15);
    }

    #[test]
    fn test_curried() {
        let add3 = add_curried(3);
        assert_eq!(add3(7), 10);
        assert_eq!(add3(0), 3);
    }

    #[test]
    fn test_sections() {
        assert_eq!(double()(7), 14);
        assert_eq!(increment()(9), 10);
        assert_eq!(halve()(20), 10);
    }

    #[test]
    fn test_pipeline() {
        let d = double();
        let i = increment();
        let h = halve();
        let result = pipeline(6, &[&d, &i, &h]);
        // 6 * 2 = 12, + 1 = 13, / 2 = 6
        assert_eq!(result, 6);
    }

    #[test]
    fn test_celsius() {
        assert_eq!(celsius_of_fahrenheit(212), 900); // 212*5 - 160 = 900
        // Note: integer arithmetic, not actual Celsius conversion
    }

    #[test]
    fn test_curry_uncurry() {
        let curried_add = curry(add);
        assert_eq!(curried_add(3)(4), 7);
    }
}
let add x y = x + y
let add5 = add 5

let add_tup (x, y) = x + y

let curry   f x y = f (x, y)
let uncurry f (x, y) = f x y

let double    = ( * ) 2
let increment = ( + ) 1
let halve     = Fun.flip ( / ) 2

let scale_and_shift ~scale ~shift x = x * scale + shift
let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)

let () =
  assert (add5 10 = 15);
  assert (double 7 = 14);
  assert (halve 20 = 10);
  let pipeline = [double; increment; halve] in
  let result = List.fold_left (fun acc f -> f acc) 6 pipeline in
  assert (result = 6);
  print_endline "All assertions passed."

๐Ÿ“Š Detailed Comparison

Currying, Partial Application, and Sections: OCaml vs Rust

The Core Insight

Currying is OCaml's bread and butter โ€” every multi-argument function is actually a chain of single-argument functions. Rust made a deliberate design choice NOT to curry by default, favoring explicit closures instead. This reveals a philosophical difference: OCaml optimizes for functional composition; Rust optimizes for clarity and zero-cost abstraction.

OCaml Approach

In OCaml, `let add x y = x + y` is syntactic sugar for `let add = fun x -> fun y -> x + y`. This means `add 5` naturally returns a function `fun y -> 5 + y`. Operator sections like `( * ) 2` partially apply multiplication. `Fun.flip` swaps argument order for operators like division. Labeled arguments (`~scale ~shift`) enable partial application in any order. This all composes beautifully for pipeline-style programming.

Rust Approach

Rust functions take all arguments at once: `fn add(x: i32, y: i32) -> i32`. Partial application requires explicitly returning a closure: `fn add5() -> impl Fn(i32) -> i32 { |y| add(5, y) }`. The `move` keyword captures variables by value. Generic `curry`/`uncurry` converters are possible but require `Box<dyn Fn>` for the intermediate closure (due to Rust's requirement that function return types have known size). The tradeoff is more verbosity for complete control over capture and allocation.

Side-by-Side

ConceptOCamlRust
DefaultAll functions curriedAll functions take full args
Partial application`add 5` (free)`\y\add(5, y)` (closure)
Operator section`( ) 2``\x\x 2`
Flip`Fun.flip ( / ) 2`No built-in (write closure)
Labeled args`~scale ~shift`Not available (use structs)
Pipeline`List.fold_left``iter().fold()` or explicit
Return functionNatural (currying)`impl Fn(...)` or `Box<dyn Fn(...)>`

What Rust Learners Should Notice

  • OCaml's currying is zero-cost because the compiler knows the full type; Rust closures may allocate when boxed (`Box<dyn Fn>`) but `impl Fn` closures are monomorphized and zero-cost
  • `move |y| ...` captures variables by value โ€” essential when the closure outlives its creation scope
  • Rust's lack of currying is intentional: explicit closures are clearer about what's captured and how
  • `impl Fn(i32) -> i32` as a return type is Rust's way of saying "returns some closure" without boxing โ€” it's a zero-cost abstraction
  • For labeled/named arguments, Rust uses the builder pattern or struct arguments โ€” different idiom, same result

Further Reading

  • [The Rust Book โ€” Closures](https://doc.rust-lang.org/book/ch13-01-closures.html)
  • [OCaml Higher-Order Functions](https://cs3110.github.io/textbook/chapters/hop/higher_order.html)