ExamplesBy LevelBy TopicLearning Paths
1163 Intermediate

Currying, Partial Application, and Operator Sections

Higher-Order Functions

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