ExamplesBy LevelBy TopicLearning Paths
1107 Intermediate

Currying, Partial Application, and Sections

Higher-Order Functions

Tutorial Video

Text description (accessibility)

This video demonstrates the "Currying, Partial Application, and Sections" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Higher-Order Functions. Demonstrate how OCaml's automatic currying and partial application translate to Rust, where functions take all arguments at once but closures enable the same specialisation patterns. Key difference from OCaml: 1. **Default currying:** OCaml functions are automatically curried; Rust requires an explicit closure wrapper returning another closure.

Tutorial

The Problem

Demonstrate how OCaml's automatic currying and partial application translate to Rust, where functions take all arguments at once but closures enable the same specialisation patterns.

🎯 Learning Outcomes

  • • How move closures in Rust replicate OCaml's automatic partial application
  • • Why Rust needs Box<dyn Fn> to implement generic curry/uncurry converters
  • • How named functions serve as operator sections when placed in &[fn] slices
  • • How two-parameter closures replace OCaml's labeled-argument partial application
  • 🦀 The Rust Way

    Rust functions take all arguments simultaneously; partial application is written explicitly as a move closure. fn partial_add(x: i32) -> impl Fn(i32) -> i32 captures x in a closure and returns it. Generic curry/uncurry require Box<dyn Fn> for the inner closure because unnameable closure types cannot be expressed as impl Fn inside another closure return. Operator sections become named fn items that match fn(i32) -> i32 and can be collected into a slice for pipeline processing with fold.

    Code Example

    fn partial_add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    let add5 = partial_add(5);   // specialised adder
    
    fn double(x: i32) -> i32    { x * 2 }
    fn increment(x: i32) -> i32 { x + 1 }
    fn halve(x: i32) -> i32     { x / 2 }
    
    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

  • Default currying: OCaml functions are automatically curried; Rust requires an explicit closure wrapper returning another closure.
  • Partial application syntax: OCaml: add 5; Rust: move |y| add(5, y) or partial_add(5).
  • Generic higher-order converters: OCaml's curry/uncurry are naturally polymorphic; Rust requires Box<dyn Fn> for the inner returned closure due to unnameable closure types.
  • Operator sections: OCaml uses ( * ) 2 or Fun.flip ( / ) 2; Rust uses named fn items or inline closures like |x| x * 2.
  • OCaml Approach

    Every OCaml function is curried by default: let add x y = x + y has type int -> int -> int, meaning applying it to one argument returns a new function. let add5 = add 5 literally creates a specialised adder at zero cost. Operator sections like ( * ) 2 and Fun.flip ( / ) 2 use the same mechanism. Labeled arguments (~scale ~shift) allow partial application in any order.

    Full Source

    #![allow(clippy::all)]
    //! Currying, partial application, and operator sections in Rust.
    //!
    //! OCaml functions are curried by default — `let add x y = x + y` accepts
    //! one argument and returns a function waiting for the second. Rust functions
    //! take all arguments at once, but the same patterns emerge naturally with
    //! `move` closures and higher-order functions.
    
    // ---------------------------------------------------------------------------
    // 1. Partial application via closures — idiomatic Rust
    // ---------------------------------------------------------------------------
    
    /// Returns an adder function with `x` fixed as the first operand.
    ///
    /// OCaml: `let add5 = add 5` — automatic partial application.
    /// Rust: explicit `move` closure that captures `x`.
    pub fn partial_add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    
    /// Tupled variant — takes both arguments as a pair.
    ///
    /// OCaml: `let add_tup (x, y) = x + y` — destructuring in the parameter.
    /// Rust: identical syntax via irrefutable pattern in the argument position.
    pub fn add_tup((x, y): (i32, i32)) -> i32 {
        x + y
    }
    
    // ---------------------------------------------------------------------------
    // 2. curry / uncurry — converting between calling conventions
    // ---------------------------------------------------------------------------
    
    /// Converts a tupled function `(A, B) → C` into a curried function `A → (B → C)`.
    ///
    /// OCaml: `let curry f x y = f (x, y)`
    ///
    /// The inner closure is heap-allocated (`Box<dyn Fn>`) because returning
    /// `impl Fn` from inside a `move` closure is not stable without boxing.
    /// Each call to the outer closure clones `f` so the inner box can own it.
    pub fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
    where
        A: Clone + '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.clone(), b)))
        }
    }
    
    /// Converts a curried function `A → (B → C)` into a tupled function `(A, B) → C`.
    ///
    /// OCaml: `let uncurry f (x, y) = f x y`
    ///
    /// `G` captures the concrete (but opaque) type returned by `f(a)`.
    /// The resulting closure is `Fn` because neither `f` nor the temporary `G`
    /// value are consumed between calls.
    pub fn uncurry<A, B, C, F, G>(f: F) -> impl Fn((A, B)) -> C
    where
        F: Fn(A) -> G,
        G: Fn(B) -> C,
    {
        move |(a, b)| f(a)(b)
    }
    
    // ---------------------------------------------------------------------------
    // 3. Operator sections — Rust equivalent of OCaml's `( * ) 2`, `( + ) 1`
    // ---------------------------------------------------------------------------
    
    /// Doubles its argument. OCaml: `let double = ( * ) 2`.
    pub fn double(x: i32) -> i32 {
        x * 2
    }
    
    /// Adds 1 to its argument. OCaml: `let increment = ( + ) 1`.
    pub fn increment(x: i32) -> i32 {
        x + 1
    }
    
    /// Halves its argument using integer division.
    /// OCaml: `let halve = Fun.flip ( / ) 2` — `flip` swaps argument order so 2
    /// becomes the *divisor*, not the dividend.
    pub fn halve(x: i32) -> i32 {
        x / 2
    }
    
    // ---------------------------------------------------------------------------
    // 4. Labeled-argument partial application
    // ---------------------------------------------------------------------------
    
    /// Builds a linear transform `x * scale + shift` with fixed `scale` and `shift`.
    ///
    /// OCaml uses labeled arguments for any-order partial application:
    /// ```text
    /// let scale_and_shift ~scale ~shift x = x * scale + shift
    /// let celsius_of_fahrenheit = scale_and_shift ~scale:5 ~shift:(-160)
    /// ```
    /// Rust achieves the same result with a closure capturing both parameters.
    pub fn scale_and_shift(scale: i32, shift: i32) -> impl Fn(i32) -> i32 {
        move |x| x * scale + shift
    }
    
    // ---------------------------------------------------------------------------
    // 5. Pipeline helper
    // ---------------------------------------------------------------------------
    
    /// Applies a sequence of transformations to an initial value, left to right.
    ///
    /// OCaml: `List.fold_left (fun acc f -> f acc) init pipeline`
    pub fn apply_pipeline(init: i32, pipeline: &[fn(i32) -> i32]) -> i32 {
        pipeline.iter().fold(init, |acc, f| f(acc))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partial_add_specialises_adder() {
            let add5 = partial_add(5);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_partial_add_zero_and_negative_base() {
            assert_eq!(partial_add(0)(42), 42);
            assert_eq!(partial_add(-7)(7), 0);
            assert_eq!(partial_add(100)(100), 200);
        }
    
        #[test]
        fn test_add_tup_various_pairs() {
            assert_eq!(add_tup((3, 4)), 7);
            assert_eq!(add_tup((0, 0)), 0);
            assert_eq!(add_tup((-1, 1)), 0);
            assert_eq!(add_tup((-5, -5)), -10);
        }
    
        #[test]
        fn test_curry_wraps_tupled_function() {
            let curried = curry(|(x, y): (i32, i32)| x + y);
            assert_eq!(curried(3)(4), 7);
            assert_eq!(curried(0)(0), 0);
            assert_eq!(curried(-1)(1), 0);
        }
    
        #[test]
        fn test_curry_creates_reusable_partial() {
            let curried = curry(|(x, y): (i32, i32)| x * y);
            let triple = curried(3);
            assert_eq!(triple(4), 12);
            assert_eq!(triple(10), 30);
            assert_eq!(triple(0), 0);
        }
    
        #[test]
        fn test_uncurry_wraps_curried_function() {
            let tupled = uncurry(|x: i32| move |y: i32| x + y);
            assert_eq!(tupled((3, 4)), 7);
            assert_eq!(tupled((0, 5)), 5);
            assert_eq!(tupled((-1, 1)), 0);
        }
    
        #[test]
        fn test_operator_sections_double() {
            assert_eq!(double(7), 14);
            assert_eq!(double(0), 0);
            assert_eq!(double(-3), -6);
        }
    
        #[test]
        fn test_operator_sections_increment_and_halve() {
            assert_eq!(increment(9), 10);
            assert_eq!(increment(-1), 0);
            assert_eq!(halve(20), 10);
            assert_eq!(halve(7), 3); // integer division truncates
        }
    
        #[test]
        fn test_pipeline_fold() {
            // 6 →*2→ 12 →+1→ 13 →/2→ 6 (integer division)
            assert_eq!(apply_pipeline(6, &[double, increment, halve]), 6);
            // 1 →*2→ 2 →*2→ 4 →*2→ 8
            assert_eq!(apply_pipeline(1, &[double, double, double]), 8);
            // empty pipeline is identity
            assert_eq!(apply_pipeline(42, &[]), 42);
        }
    
        #[test]
        fn test_scale_and_shift_partial_application() {
            // celsius_of_fahrenheit via partial application: f * 5 - 160
            // (note: full Celsius conversion requires a subsequent / 9 step;
            //  the focus here is the partial-application pattern, not accuracy)
            let celsius_of_fahrenheit = scale_and_shift(5, -160);
            assert_eq!(celsius_of_fahrenheit(32), 0); // 32*5 - 160 = 0
            assert_eq!(celsius_of_fahrenheit(212), 900); // 212*5 - 160 = 900
        }
    
        #[test]
        fn test_scale_and_shift_general() {
            let double_plus_one = scale_and_shift(2, 1);
            assert_eq!(double_plus_one(0), 1); // 0*2 + 1
            assert_eq!(double_plus_one(5), 11); // 5*2 + 1
            let negate = scale_and_shift(-1, 0);
            assert_eq!(negate(7), -7);
            assert_eq!(negate(-3), 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partial_add_specialises_adder() {
            let add5 = partial_add(5);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_partial_add_zero_and_negative_base() {
            assert_eq!(partial_add(0)(42), 42);
            assert_eq!(partial_add(-7)(7), 0);
            assert_eq!(partial_add(100)(100), 200);
        }
    
        #[test]
        fn test_add_tup_various_pairs() {
            assert_eq!(add_tup((3, 4)), 7);
            assert_eq!(add_tup((0, 0)), 0);
            assert_eq!(add_tup((-1, 1)), 0);
            assert_eq!(add_tup((-5, -5)), -10);
        }
    
        #[test]
        fn test_curry_wraps_tupled_function() {
            let curried = curry(|(x, y): (i32, i32)| x + y);
            assert_eq!(curried(3)(4), 7);
            assert_eq!(curried(0)(0), 0);
            assert_eq!(curried(-1)(1), 0);
        }
    
        #[test]
        fn test_curry_creates_reusable_partial() {
            let curried = curry(|(x, y): (i32, i32)| x * y);
            let triple = curried(3);
            assert_eq!(triple(4), 12);
            assert_eq!(triple(10), 30);
            assert_eq!(triple(0), 0);
        }
    
        #[test]
        fn test_uncurry_wraps_curried_function() {
            let tupled = uncurry(|x: i32| move |y: i32| x + y);
            assert_eq!(tupled((3, 4)), 7);
            assert_eq!(tupled((0, 5)), 5);
            assert_eq!(tupled((-1, 1)), 0);
        }
    
        #[test]
        fn test_operator_sections_double() {
            assert_eq!(double(7), 14);
            assert_eq!(double(0), 0);
            assert_eq!(double(-3), -6);
        }
    
        #[test]
        fn test_operator_sections_increment_and_halve() {
            assert_eq!(increment(9), 10);
            assert_eq!(increment(-1), 0);
            assert_eq!(halve(20), 10);
            assert_eq!(halve(7), 3); // integer division truncates
        }
    
        #[test]
        fn test_pipeline_fold() {
            // 6 →*2→ 12 →+1→ 13 →/2→ 6 (integer division)
            assert_eq!(apply_pipeline(6, &[double, increment, halve]), 6);
            // 1 →*2→ 2 →*2→ 4 →*2→ 8
            assert_eq!(apply_pipeline(1, &[double, double, double]), 8);
            // empty pipeline is identity
            assert_eq!(apply_pipeline(42, &[]), 42);
        }
    
        #[test]
        fn test_scale_and_shift_partial_application() {
            // celsius_of_fahrenheit via partial application: f * 5 - 160
            // (note: full Celsius conversion requires a subsequent / 9 step;
            //  the focus here is the partial-application pattern, not accuracy)
            let celsius_of_fahrenheit = scale_and_shift(5, -160);
            assert_eq!(celsius_of_fahrenheit(32), 0); // 32*5 - 160 = 0
            assert_eq!(celsius_of_fahrenheit(212), 900); // 212*5 - 160 = 900
        }
    
        #[test]
        fn test_scale_and_shift_general() {
            let double_plus_one = scale_and_shift(2, 1);
            assert_eq!(double_plus_one(0), 1); // 0*2 + 1
            assert_eq!(double_plus_one(5), 11); // 5*2 + 1
            let negate = scale_and_shift(-1, 0);
            assert_eq!(negate(7), -7);
            assert_eq!(negate(-3), 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: Currying, Partial Application, and Sections

    Side-by-Side Code

    OCaml — partial application (automatic)

    let add x y = x + y
    let add5 = add 5        (* partial application: add5 : int -> int *)
    
    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)
    

    Rust (idiomatic) — partial application via closures

    fn partial_add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    let add5 = partial_add(5);   // specialised adder
    
    fn double(x: i32) -> i32    { x * 2 }
    fn increment(x: i32) -> i32 { x + 1 }
    fn halve(x: i32) -> i32     { x / 2 }
    
    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);
    

    OCaml — generic curry/uncurry

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

    Rust — generic curry/uncurry

    // curry: (A,B)->C  →  A -> Box<dyn Fn(B)->C>
    fn curry<A, B, C, F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>
    where
        A: Clone + '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.clone(), b)))
        }
    }
    
    // uncurry: (A -> B -> C) → (A,B) -> C
    fn uncurry<A, B, C, F, G>(f: F) -> impl Fn((A, B)) -> C
    where
        F: Fn(A) -> G,
        G: Fn(B) -> C,
    {
        move |(a, b)| f(a)(b)
    }
    

    Pipeline comparison

    (* OCaml *)
    let pipeline = [double; increment; halve]
    let result = List.fold_left (fun acc f -> f acc) 6 pipeline
    (* 6 → 12 → 13 → 6 *)
    
    // Rust
    let result = apply_pipeline(6, &[double, increment, halve]);
    // 6 → 12 → 13 → 6
    fn apply_pipeline(init: i32, pipeline: &[fn(i32) -> i32]) -> i32 {
        pipeline.iter().fold(init, |acc, f| f(acc))
    }
    

    Type Signatures

    ConceptOCamlRust
    Curried addval add : int -> int -> intfn partial_add(x: i32) -> impl Fn(i32) -> i32
    Partial applicationlet add5 = add 5let add5 = partial_add(5);
    Tupled addval add_tup : int * int -> intfn add_tup((x,y): (i32,i32)) -> i32
    curry('a*'b->'c) -> 'a -> 'b -> 'cfn curry<A,B,C,F>(f:F) -> impl Fn(A)->Box<dyn Fn(B)->C>
    uncurry('a->'b->'c) -> 'a*'b -> 'cfn uncurry<A,B,C,F,G>(f:F) -> impl Fn((A,B))->C
    Operator section( * ) 2 : int -> intfn double(x: i32) -> i32 { x * 2 }
    Flip sectionFun.flip ( / ) 2fn halve(x: i32) -> i32 { x / 2 }
    Labeled partialscale_and_shift ~scale:5 ~shift:(-160)scale_and_shift(5, -160)

    Key Insights

  • Auto-curry vs explicit closure: OCaml curries every function for free;
  • Rust requires writing the closure explicitly. The intent is the same but Rust makes the allocation and capture visible.

  • **Box<dyn Fn> for generic curry:** Returning impl Fn from inside a
  • move closure is not stable in Rust without boxing, because the inner closure has an unnameable type. Box<dyn Fn(B) -> C> is the idiomatic workaround and makes the heap allocation explicit.

  • **Fun.flip vs argument order:** OCaml's Fun.flip ( / ) 2 fixes 2 as
  • the divisor (right argument). Rust achieves this by just writing |x| x / 2 — no combinator needed because the argument order is already explicit in the closure body.

  • Labeled arguments vs positional: OCaml's ~scale ~shift labels let
  • callers supply arguments in any order. Rust has no labeled arguments; the same effect comes from positional parameters plus a returned closure.

  • **fn pointers vs closures in slices:** OCaml's [double; increment; halve]
  • is a list of first-class functions. Rust's &[fn(i32) -> i32] holds function pointers (not closures), which is why double, increment, and halve are declared as fn items rather than let bindings with impl Fn types — function items coerce to fn pointers, closures do not.

    When to Use Each Style

    **Use idiomatic Rust (move closure returning impl Fn)** when you need partial application within a single codebase and performance matters — zero heap allocation, monomorphised types, no virtual dispatch.

    **Use Box<dyn Fn> (as in curry)** when you need a generic adapter that works with arbitrary function types at runtime, or when you must store heterogeneous closures in a collection.

    Exercises

  • Use partial application to create a family of multiplier functions (double, triple, times_n) from a single curried multiply: i32 -> i32 -> i32.
  • Implement section_left and section_right combinators that fix the left or right argument of a binary function, and use them to adapt str::contains into prefix/suffix checkers.
  • Write a pipeline that reads a list of raw log strings, uses partially applied predicates to filter by severity level, partially applied formatters to normalize each line, and outputs the result — with each step expressed as a point-free composition.
  • Open Source Repos