ExamplesBy LevelBy TopicLearning Paths
005 Intermediate

005 — Currying and Partial Application

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "005 — Currying and Partial Application" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Currying (named after mathematician Haskell Curry, though independently discovered by Moses Schonfinkel) is the technique of transforming a multi-argument function into a chain of single-argument functions. Key difference from OCaml: 1. **Default currying**: OCaml curries all functions automatically; every `let f a b = ...` is implicitly `let f = fun a

Tutorial

The Problem

Currying (named after mathematician Haskell Curry, though independently discovered by Moses Schonfinkel) is the technique of transforming a multi-argument function into a chain of single-argument functions. add(a, b) becomes add(a)(b), where add(5) returns a new function that adds 5 to its argument. This makes partial application — fixing some arguments now and the rest later — a natural consequence of function application.

Currying is the foundation of functional composition. It enables point-free style, makes predicate factories (like greater_than(threshold)) trivial, and is how Haskell and OCaml's standard libraries are designed. In practice it appears in React's higher-order components, Lodash's _.curry, and functional Redux reducers.

🎯 Learning Outcomes

  • • Understand the difference between currying (transforming function arity) and partial application (fixing arguments)
  • • Return closures from functions to achieve curried-style APIs in Rust
  • • Use move closures to capture environment by value
  • • Build predicate factories and transformation pipelines with partial application
  • • Understand why OCaml's automatic currying requires explicit closures in Rust
  • • Recognize that OCaml's standard library is designed around currying — List.map, List.filter, and List.fold_left all take their function argument first for partial application
  • Code Example

    fn add(n: i64) -> impl Fn(i64) -> i64 {
        move |x| x + n
    }
    let add5 = add(5);

    Key Differences

  • Default currying: OCaml curries all functions automatically; every let f a b = ... is implicitly let f = fun a -> fun b -> .... Rust requires explicit closure-returning functions.
  • Closure syntax: OCaml: fun x -> x + n. Rust: move |x| x + n. The move keyword is required in Rust to transfer ownership of captured variables into the closure.
  • Type inference: Both infer closure types from usage, but Rust's impl Fn(T) -> T return type must be written explicitly when returning closures from functions.
  • **FnOnce vs Fn**: Rust distinguishes closures that can be called once (FnOnce), once at a time (FnMut), and multiple times (Fn). OCaml has no such distinction.
  • Automatic vs explicit: OCaml curries all functions automatically. Rust requires explicit closure-returning functions.
  • Inference: OCaml infers the type of add 5 as int -> int without annotation. Rust requires impl Fn(i64) -> i64 as the return type annotation.
  • Move semantics: Rust closures that capture their environment by value need move. OCaml's closures capture implicitly with GC ownership.
  • **Clone for reuse:** Rust's partial requires A: Clone because the captured argument may be used multiple times. OCaml doesn't need this — sharing is automatic.
  • **Box<dyn Fn> for dynamic dispatch:** Returning closures from functions sometimes requires Box<dyn Fn> in Rust when the concrete type is unnameable. OCaml closures always erase to a uniform representation.
  • OCaml Approach

    In OCaml, ALL multi-argument functions are automatically curried. let add a b = a + b is syntactic sugar for let add = fun a -> fun b -> a + b. Partial application is free: let add5 = add 5 immediately produces a function. let double = ( * ) 2 partially applies multiplication. The standard library exploits this everywhere: List.map (fun x -> x + 1) partially applies List.map.

    Full Source

    #![allow(clippy::all)]
    /// Currying and Partial Application: OCaml's default vs Rust's closures.
    ///
    /// In OCaml, ALL functions are curried by default. `let add a b = a + b` is
    /// really `let add = fun a -> fun b -> a + b`. Rust doesn't curry automatically,
    /// but closures achieve the same effect.
    
    // ── Idiomatic Rust: closures for partial application ────────────────────────
    
    /// Returns a closure that adds `n` to its argument.
    /// This is the Rust equivalent of OCaml's `let add n = fun x -> x + n`
    pub fn add(n: i64) -> impl Fn(i64) -> i64 {
        move |x| x + n
    }
    
    /// Returns a closure that multiplies by `n`
    pub fn multiply_by(n: i64) -> impl Fn(i64) -> i64 {
        move |x| x * n
    }
    
    /// Generic partial application: fix the first argument of a two-arg function.
    /// In OCaml this is free; in Rust we build it explicitly.
    pub fn partial<A, B, C, F>(f: F, a: A) -> impl Fn(B) -> C
    where
        F: Fn(A, B) -> C,
        A: Clone,
    {
        move |b| f(a.clone(), b)
    }
    
    // ── Demonstrating curried-style APIs ────────────────────────────────────────
    
    /// A curried comparison: returns a predicate
    pub fn greater_than(threshold: i64) -> impl Fn(&i64) -> bool {
        move |x| *x > threshold
    }
    
    /// Apply a list of transformations (each is a curried function result)
    pub fn apply_all(value: i64, transforms: &[Box<dyn Fn(i64) -> i64>]) -> i64 {
        transforms.iter().fold(value, |acc, f| f(acc))
    }
    
    // ── Functional style: simulating currying with nested closures ──────────────
    
    /// Fully curried three-argument function
    /// OCaml: `let f a b c = a + b + c` → `f 1` returns a function, `f 1 2` returns a function
    pub fn curried_add3(a: i64) -> Box<dyn Fn(i64) -> Box<dyn Fn(i64) -> i64>> {
        Box::new(move |b| Box::new(move |c| a + b + c))
    }
    
    // ── Practical example: building predicates ──────────────────────────────────
    
    /// Filter with a curried predicate builder
    pub fn between(low: i64, high: i64) -> impl Fn(&i64) -> bool {
        move |x| *x >= low && *x <= high
    }
    
    pub fn filter_between(list: &[i64], low: i64, high: i64) -> Vec<i64> {
        list.iter().copied().filter(between(low, high)).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_add_partial() {
            let add5 = add(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-5), 0);
        }
    
        #[test]
        fn test_multiply_by() {
            let double = multiply_by(2);
            let triple = multiply_by(3);
            assert_eq!(double(7), 14);
            assert_eq!(triple(7), 21);
        }
    
        #[test]
        fn test_generic_partial() {
            let pow = |base: i64, exp: u32| base.pow(exp);
            let square = partial(pow, 2_i64); // partially apply base=2
                                              // Note: this gives 2^exp, not x^2
            assert_eq!(square(10), 1024); // 2^10
        }
    
        #[test]
        fn test_greater_than_as_predicate() {
            let data = vec![1, 5, 3, 8, 2, 9];
            let big: Vec<_> = data
                .iter()
                .filter(|x| greater_than(4)(x))
                .copied()
                .collect();
            assert_eq!(big, vec![5, 8, 9]);
        }
    
        #[test]
        fn test_curried_add3() {
            let f = curried_add3(1);
            let g = f(2);
            assert_eq!(g(3), 6);
            assert_eq!(curried_add3(10)(20)(30), 60);
        }
    
        #[test]
        fn test_filter_between() {
            assert_eq!(filter_between(&[1, 2, 3, 4, 5, 6], 2, 5), vec![2, 3, 4, 5]);
            assert_eq!(filter_between(&[], 0, 10), Vec::<i64>::new());
            assert_eq!(filter_between(&[100], 0, 10), Vec::<i64>::new());
        }
    
        #[test]
        fn test_apply_all_transforms() {
            let transforms: Vec<Box<dyn Fn(i64) -> i64>> = vec![
                Box::new(add(10)),
                Box::new(multiply_by(2)),
                Box::new(add(-1)),
            ];
            // 5 → +10 = 15 → *2 = 30 → -1 = 29
            assert_eq!(apply_all(5, &transforms), 29);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_add_partial() {
            let add5 = add(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-5), 0);
        }
    
        #[test]
        fn test_multiply_by() {
            let double = multiply_by(2);
            let triple = multiply_by(3);
            assert_eq!(double(7), 14);
            assert_eq!(triple(7), 21);
        }
    
        #[test]
        fn test_generic_partial() {
            let pow = |base: i64, exp: u32| base.pow(exp);
            let square = partial(pow, 2_i64); // partially apply base=2
                                              // Note: this gives 2^exp, not x^2
            assert_eq!(square(10), 1024); // 2^10
        }
    
        #[test]
        fn test_greater_than_as_predicate() {
            let data = vec![1, 5, 3, 8, 2, 9];
            let big: Vec<_> = data
                .iter()
                .filter(|x| greater_than(4)(x))
                .copied()
                .collect();
            assert_eq!(big, vec![5, 8, 9]);
        }
    
        #[test]
        fn test_curried_add3() {
            let f = curried_add3(1);
            let g = f(2);
            assert_eq!(g(3), 6);
            assert_eq!(curried_add3(10)(20)(30), 60);
        }
    
        #[test]
        fn test_filter_between() {
            assert_eq!(filter_between(&[1, 2, 3, 4, 5, 6], 2, 5), vec![2, 3, 4, 5]);
            assert_eq!(filter_between(&[], 0, 10), Vec::<i64>::new());
            assert_eq!(filter_between(&[100], 0, 10), Vec::<i64>::new());
        }
    
        #[test]
        fn test_apply_all_transforms() {
            let transforms: Vec<Box<dyn Fn(i64) -> i64>> = vec![
                Box::new(add(10)),
                Box::new(multiply_by(2)),
                Box::new(add(-1)),
            ];
            // 5 → +10 = 15 → *2 = 30 → -1 = 29
            assert_eq!(apply_all(5, &transforms), 29);
        }
    }

    Deep Comparison

    Currying and Partial Application: OCaml vs Rust

    The Core Insight

    Currying is automatic and pervasive in OCaml — every function is curried, and partial application is free. Rust requires explicit closures, but the resulting pattern is equally powerful. Understanding this difference reveals how each language thinks about functions as values.

    OCaml Approach

    In OCaml, let add a b = a + b is syntactic sugar for let add = fun a -> fun b -> a + b. Every multi-argument function is actually a chain of single-argument functions:

    let add5 = add 5           (* partially apply: returns fun b -> 5 + b *)
    let result = add5 3         (* 8 *)
    let big = List.filter (greater_than 4) [1;5;3;8]  (* [5;8] *)
    

    This makes function composition and predicate building incredibly natural. The |> pipe operator complements currying beautifully.

    Rust Approach

    Rust functions are not automatically curried. To achieve partial application, you return closures:

    fn add(n: i64) -> impl Fn(i64) -> i64 {
        move |x| x + n
    }
    let add5 = add(5);
    

    The move keyword captures n by value (ownership transfer). For generic partial application, you need explicit lifetime and trait bounds — more verbose but equally expressive.

    Key Differences

    AspectOCamlRust
    CurryingAutomatic (all functions)Manual (return closures)
    Partial applicationFree (f a)Explicit (move \|x\| ...)
    Closure captureAutomatic (GC)move or borrow (ownership)
    Function types'a -> 'b -> 'cFn(A) -> impl Fn(B) -> C
    Type inferenceFullPartial (return types need annotation)
    PerformanceAllocation per partial appZero-cost when inlined
    Trait boundsNone neededFn/FnMut/FnOnce

    What Rust Learners Should Notice

  • • **Fn vs FnMut vs FnOnce**: These three traits determine how a closure captures its environment. Fn borrows immutably, FnMut borrows mutably, FnOnce consumes. OCaml doesn't have this distinction (GC handles it).
  • • **impl Fn vs Box<dyn Fn>**: impl Fn is monomorphized (zero-cost, can't be stored in collections). Box<dyn Fn> is heap-allocated (can be stored, has runtime cost). OCaml closures are always heap-allocated.
  • • **move is the key**: Without move, closures borrow from their environment. With move, they take ownership. This is the ownership system at work in closures.
  • Curried APIs are less idiomatic in Rust: While possible, Rust code typically uses method chaining and builder patterns rather than curried functions.
  • Further Reading

  • • [The Rust Book — Closures](https://doc.rust-lang.org/book/ch13-01-closures.html)
  • • [OCaml Manual — Functions](https://v2.ocaml.org/manual/expr.html#ss:expr-function)
  • • [Rust Reference — Closure types](https://doc.rust-lang.org/reference/types/closure.html)
  • Exercises

  • Pipeline factory: Write make_pipeline(steps: Vec<Box<dyn Fn(i64) -> i64>>) -> impl Fn(i64) -> i64 that returns a function composing all steps in sequence.
  • Memoized partial: Write a version of partial that memoizes the result of calling the inner function with a particular a value using a HashMap.
  • Uncurry: Write uncurry<A,B,C>(f: impl Fn(A) -> impl Fn(B) -> C) -> impl Fn(A, B) -> C that converts a curried function back into a two-argument function.
  • Memoized partial application: Implement a once_with(f: impl FnOnce() -> T) -> impl Fn() -> T that calls f once and caches the result (hint: use std::cell::OnceCell).
  • Uncurry: Write uncurry<A, B, C>(f: impl Fn(A) -> impl Fn(B) -> C) -> impl Fn(A, B) -> C that converts a curried function back to a two-argument function.
  • Open Source Repos