ExamplesBy LevelBy TopicLearning Paths
1093 Intermediate GitHub

Currying and Partial Application

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Currying and Partial Application" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional 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