ExamplesBy LevelBy TopicLearning Paths
1163 Intermediate GitHub

Currying, Partial Application, and Operator Sections

Functional Programming

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

  • • How to express partial application in Rust via closures that capture arguments
  • • Why Box<dyn Fn> is needed when returning closures from generic higher-order functions
  • • How curry and uncurry convert between tupled and sequential calling styles
  • • How flip (argument-order swap) enables operator sections like halve = flip(div)(2)
  • • How to fold a slice of function pointers to build a processing pipeline
  • 🦀 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

  • Currying: OCaml functions are curried by default; Rust closures capture
  • arguments explicitly with move |y| x + y.

  • Returning closures: OCaml returns curried functions transparently; Rust
  • needs Box<dyn Fn> to heap-allocate closures whose concrete type is unknown at the call site.

  • Labeled arguments: OCaml's ~scale and ~shift allow partial
  • application in any order; Rust uses positional parameters and wraps calls in closures to fix specific arguments.

  • Operator sections: OCaml's ( * ) 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
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Binary addval add : int -> int -> intfn add(x: i32, y: i32) -> i32
    Partial addval add5 : int -> intlet add5: impl Fn(i32) -> i32 = make_adder(5)
    Tupled addval add_tup : int * int -> intfn add_tup((x,y): (i32,i32)) -> i32
    curryval curry : ('a*'b->'c) -> 'a -> 'b -> 'cfn curry<A,B,C,F>(f:F) -> impl Fn(A) -> Box<dyn Fn(B)->C>
    uncurryval uncurry : ('a->'b->'c) -> 'a*'b -> 'cfn uncurry<A,B,C,G,F>(f:F) -> impl Fn((A,B))->C
    flipval flip : ('a->'b->'c) -> 'b -> 'a -> 'cfn 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

  • Currying is automatic in OCaml, explicit in Rust. Every OCaml function
  • 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.)

  • Lifetime and Clone bounds flow from ownership. 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.

  • Labeled arguments vs positional parameters. OCaml's ~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.

  • Function pointers for pipelines. OCaml's [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

  • Write a curried zip_with: (A -> B -> C) -> [A] -> [B] -> [C] and use partial application to create a vector addition function from a curried add.
  • Implement uncurry that converts a curried function A -> B -> C back into a two-argument function (A, B) -> C.
  • Use currying, partial application, and composition together to build a small expression evaluator: a curried eval_binop parameterized by operator and operands, composed with a parser that splits an infix expression string.
  • Open Source Repos