ExamplesBy LevelBy TopicLearning Paths
947 Intermediate

947 Currying Partial

Functional Programming

Tutorial

The Problem

Explore currying, partial application, and function sections in Rust. Unlike OCaml, where all functions are curried by default, Rust functions take all arguments at once. Partial application is achieved through closures that capture some arguments. Implement a curry converter, an uncurry converter, and a pipeline function that folds a value through a chain of unary functions.

🎯 Learning Outcomes

  • • Understand that Rust functions are NOT curried by default; closures provide partial application
  • • Implement add_curried(x) -> impl Fn(i32) -> i32 to mimic OCaml's default currying
  • • Write a generic curry<A, B, C> converter that turns fn(A, B) -> C into Fn(A) -> Box<dyn Fn(B) -> C>
  • • Write the inverse uncurry converter
  • • Implement pipeline(init, &[&dyn Fn(i32) -> i32]) -> i32 as a fold over unary functions
  • Code Example

    #![allow(clippy::all)]
    /// 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);
        }
    }

    Key Differences

    AspectRustOCaml
    Default curryingNo — use closuresYes — all multi-arg functions are auto-curried
    Partial applicationmove |y| f(5, y)f 5 — no closure syntax needed
    Box<dyn Fn> overheadRequired for generic curried return typeNo equivalent overhead (closures are GC values)
    Pipelinefold over &[&dyn Fn]List.fold_left (fun acc f -> f acc)
    Operator sections(|x| x * 2)(( * ) 2) — more concise

    Rust's type system makes generic higher-order combinators like curry awkward — the A: Copy + 'static constraints are artifacts of ownership, not logic. For practical code, prefer direct closures over a generic curry combinator.

    OCaml Approach

    (* OCaml functions are curried by default *)
    let add x y = x + y
    let add5 = add 5          (* partial application — no closure needed *)
    let add_curried x y = x + y  (* identical to add *)
    
    let pipeline initial funcs =
      List.fold_left (fun acc f -> f acc) initial funcs
    
    (* curry/uncurry as identity since OCaml is already curried *)
    let curry f x y = f (x, y)
    let uncurry f (x, y) = f x y
    

    In OCaml, add 5 is free — no closure allocation; the runtime tracks the partial application. curry and uncurry in OCaml convert between tuple-argument and curried-argument styles, which is the reverse of Rust's use case.

    Full Source

    #![allow(clippy::all)]
    /// 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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Deep 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 applicationadd 5 (free)\|y\| add(5, y) (closure)
    Operator section( * ) 2\|x\| x * 2
    FlipFun.flip ( / ) 2No built-in (write closure)
    Labeled args~scale ~shiftNot available (use structs)
    PipelineList.fold_leftiter().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)
  • Exercises

  • Implement compose(f, g) -> impl Fn(A) -> C where compose(f, g)(x) = g(f(x)) (left-to-right composition).
  • Implement compose_n(funcs: Vec<Box<dyn Fn(i32) -> i32>>) -> impl Fn(i32) -> i32 that composes a dynamic list of functions.
  • Use add_curried to create add1, add10, add100 and apply them via pipeline.
  • Implement a memoize higher-order function that wraps Fn(i32) -> i32 with a HashMap cache.
  • Explore Rust's FnOnce, FnMut, Fn distinction: write examples that compile only with FnOnce and only with Fn.
  • Open Source Repos