ExamplesBy LevelBy TopicLearning Paths
1093 Fundamental

Currying and Partial Application

FunctionsHigher-Order Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Currying and Partial Application" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functions, Higher-Order Programming. Implement `let add x y = x + y` in Rust, showing how OCaml's automatic currying translates to Rust's explicit closures for partial application. Key difference from OCaml: 1. **Currying:** OCaml auto

Tutorial

The Problem

Implement let add x y = x + y in Rust, showing how OCaml's automatic currying translates to Rust's explicit closures for partial application.

🎯 Learning Outcomes

  • • How OCaml's auto-currying maps to Rust's impl Fn return types
  • • Using move closures to capture values for partial application
  • • Writing generic curried functions with trait bounds (Add + Copy)
  • • Building curry / uncurry combinators as higher-order functions
  • 🦀 The Rust Way

    Rust functions take all arguments at once. To get partial application, you return a closure: fn add_partial(x: i64) -> impl Fn(i64) -> i64. The move keyword transfers ownership of captured values into the closure.

    Code Example

    /// Plain two-argument function.
    fn add(x: i64, y: i64) -> i64 {
        x + y
    }
    
    /// Partial application: return a closure capturing `x`.
    fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
        move |y| x + y
    }
    
    let add5 = add_partial(5);
    assert_eq!(add5(3), 8);

    Key Differences

  • Currying: OCaml auto-curries all functions; Rust requires explicit closure returns
  • Type signatures: OCaml infers int -> int -> int; Rust needs impl Fn(i64) -> i64 return type
  • Capture semantics: OCaml closures capture by GC reference; Rust uses move for ownership transfer
  • Polymorphism: OCaml uses parametric polymorphism implicitly; Rust requires <T: Add + Copy> bounds
  • OCaml Approach

    In OCaml, let add x y = x + y is sugar for let add = fun x -> fun y -> x + y. Every function is automatically curried — add 5 returns a function. No special syntax needed for partial application.

    Full Source

    #![allow(clippy::all)]
    // # Currying and Partial Application
    //
    // OCaml: `let add x y = x + y` — all functions are automatically curried.
    // Rust has no auto-currying, but closures make partial application natural.
    
    // ---------------------------------------------------------------------------
    // Solution 1: Idiomatic Rust — closures for partial application
    // ---------------------------------------------------------------------------
    
    /// A plain two-argument function — Rust's default style.
    pub fn add(x: i64, y: i64) -> i64 {
        x + y
    }
    
    /// Returns a closure that adds `x` to its argument.
    /// This is how Rust developers do partial application: return a closure.
    pub fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
        move |y| x + y
    }
    
    // ---------------------------------------------------------------------------
    // Solution 2: Curried style — mirrors OCaml's `let add x y = x + y`
    // ---------------------------------------------------------------------------
    
    /// Fully curried: each argument returns a closure expecting the next.
    /// Closest to OCaml's automatic currying, but explicit in Rust.
    pub fn add_curried(x: i64) -> impl Fn(i64) -> i64 {
        move |y| x + y
    }
    
    /// A generic curried add for any type supporting `Add`.
    /// Shows how Rust generics replace OCaml's polymorphism here.
    pub fn add_curried_generic<T>(x: T) -> impl Fn(T) -> T
    where
        T: std::ops::Add<Output = T> + Copy,
    {
        move |y| x + y
    }
    
    // ---------------------------------------------------------------------------
    // Solution 3: Higher-order — curry any two-argument function
    // ---------------------------------------------------------------------------
    
    /// Transforms a two-argument function into a curried chain.
    /// `curry(f)` returns `|x| |y| f(x, y)`.
    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 + Copy + 'static,
    {
        move |a: A| Box::new(move |b: B| f(a, b))
    }
    
    /// The inverse: uncurry a curried function back to a two-argument function.
    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)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_add_direct() {
            assert_eq!(add(3, 4), 7);
            assert_eq!(add(-1, 1), 0);
            assert_eq!(add(0, 0), 0);
        }
    
        #[test]
        fn test_partial_application() {
            let add5 = add_partial(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-5), 0);
        }
    
        #[test]
        fn test_curried() {
            let add10 = add_curried(10);
            assert_eq!(add10(1), 11);
            assert_eq!(add10(-10), 0);
    
            // Call in one shot: add_curried(2)(3)
            assert_eq!(add_curried(2)(3), 5);
        }
    
        #[test]
        fn test_curried_generic() {
            let add_f64 = add_curried_generic(1.5_f64);
            assert!((add_f64(2.5) - 4.0).abs() < f64::EPSILON);
    
            let add_i32 = add_curried_generic(100_i32);
            assert_eq!(add_i32(23), 123);
        }
    
        #[test]
        fn test_curry_combinator() {
            let curried_add = curry(add);
            let add7 = curried_add(7);
            assert_eq!(add7(3), 10);
            assert_eq!(add7(0), 7);
        }
    
        #[test]
        fn test_uncurry_combinator() {
            let uncurried = uncurry(add_curried);
            assert_eq!(uncurried(3, 4), 7);
            assert_eq!(uncurried(0, 0), 0);
        }
    
        #[test]
        fn test_curry_with_multiply() {
            let mul = |a: i64, b: i64| a * b;
            let curried_mul = curry(mul);
            let double = curried_mul(2);
            assert_eq!(double(5), 10);
            assert_eq!(double(0), 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_add_direct() {
            assert_eq!(add(3, 4), 7);
            assert_eq!(add(-1, 1), 0);
            assert_eq!(add(0, 0), 0);
        }
    
        #[test]
        fn test_partial_application() {
            let add5 = add_partial(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-5), 0);
        }
    
        #[test]
        fn test_curried() {
            let add10 = add_curried(10);
            assert_eq!(add10(1), 11);
            assert_eq!(add10(-10), 0);
    
            // Call in one shot: add_curried(2)(3)
            assert_eq!(add_curried(2)(3), 5);
        }
    
        #[test]
        fn test_curried_generic() {
            let add_f64 = add_curried_generic(1.5_f64);
            assert!((add_f64(2.5) - 4.0).abs() < f64::EPSILON);
    
            let add_i32 = add_curried_generic(100_i32);
            assert_eq!(add_i32(23), 123);
        }
    
        #[test]
        fn test_curry_combinator() {
            let curried_add = curry(add);
            let add7 = curried_add(7);
            assert_eq!(add7(3), 10);
            assert_eq!(add7(0), 7);
        }
    
        #[test]
        fn test_uncurry_combinator() {
            let uncurried = uncurry(add_curried);
            assert_eq!(uncurried(3, 4), 7);
            assert_eq!(uncurried(0, 0), 0);
        }
    
        #[test]
        fn test_curry_with_multiply() {
            let mul = |a: i64, b: i64| a * b;
            let curried_mul = curry(mul);
            let double = curried_mul(2);
            assert_eq!(double(5), 10);
            assert_eq!(double(0), 0);
        }
    }

    Deep Comparison

    OCaml vs Rust: Currying and Partial Application

    Side-by-Side Code

    OCaml

    (* All functions are automatically curried *)
    let add x y = x + y
    
    (* Partial application — just supply fewer arguments *)
    let add5 = add 5
    
    (* Equivalent desugared form *)
    let add' = fun x -> fun y -> x + y
    

    Rust (idiomatic)

    /// Plain two-argument function.
    fn add(x: i64, y: i64) -> i64 {
        x + y
    }
    
    /// Partial application: return a closure capturing `x`.
    fn add_partial(x: i64) -> impl Fn(i64) -> i64 {
        move |y| x + y
    }
    
    let add5 = add_partial(5);
    assert_eq!(add5(3), 8);
    

    Rust (functional/recursive)

    /// Fully curried — mirrors OCaml's `fun x -> fun y -> x + y`.
    fn add_curried(x: i64) -> impl Fn(i64) -> i64 {
        move |y| x + y
    }
    
    /// Generic version with trait bounds.
    fn add_curried_generic<T: std::ops::Add<Output = T> + Copy>(x: T) -> impl Fn(T) -> T {
        move |y| x + y
    }
    
    // One-shot call reads like OCaml: add_curried(2)(3)
    assert_eq!(add_curried(2)(3), 5);
    

    Type Signatures

    ConceptOCamlRust
    Function signatureval add : int -> int -> intfn add(x: i64, y: i64) -> i64
    Curried signatureint -> int -> int (same!)fn(i64) -> impl Fn(i64) -> i64
    Partial applicationlet add5 = add 5let add5 = add_partial(5)
    Polymorphicval add : 'a -> 'a -> 'a (with (+))fn add<T: Add<Output=T> + Copy>(x: T) -> impl Fn(T) -> T
    Closure typeImplicit, GC-managedimpl Fn(i64) -> i64 (stack or heap via Box<dyn Fn>)
    Curry combinatorval curry : ('a * 'b -> 'c) -> 'a -> 'b -> 'cfn curry<A,B,C,F>(f: F) -> impl Fn(A) -> Box<dyn Fn(B) -> C>

    Key Insights

  • OCaml currying is invisible; Rust currying is explicit. In OCaml, let add x y = x + y is already fun x -> fun y -> x + y. In Rust, you must explicitly return a closure to achieve the same effect.
  • **move closures are Rust's capture mechanism.** Where OCaml's GC handles closure environments automatically, Rust's move |y| x + y transfers ownership of x into the closure — no garbage collection needed.
  • **impl Fn vs Box<dyn Fn> — static vs dynamic dispatch.** When the caller knows the concrete closure type, impl Fn gives zero-cost abstraction. When you need to store or return closures of varying types (e.g., from the curry combinator), Box<dyn Fn> provides dynamic dispatch.
  • Trait bounds replace OCaml's implicit polymorphism. OCaml's (+) works on any numeric type via ad-hoc polymorphism. Rust requires T: Add<Output = T> + Copy to express the same constraint — more verbose but explicit.
  • Rust's closures are zero-cost. Unlike OCaml's heap-allocated closure environments, Rust's move closures with impl Fn return types are monomorphized at compile time — no allocation, no indirection.
  • When to Use Each Style

    Use idiomatic Rust when: You have a fixed-arity function and occasionally want partial application — add_partial(5) is clear and efficient.

    Use curried Rust when: You're building combinator libraries or DSLs where function composition and point-free style improve readability, e.g., let transform = compose(scale(2.0), translate(1.0)).

    Exercises

  • Write a curried add3 function i32 -> i32 -> i32 -> i32 and use partial application to create an increment and a add10 specialization.
  • Implement flip — a higher-order function that takes f: A -> B -> C and returns B -> A -> C — and use it to partially apply the second argument of a binary function.
  • Use currying and partial application to build a configurable string formatter: a curried format_field that accepts a padding width, an alignment, and finally a value, returning the formatted string.
  • Open Source Repos