ExamplesBy LevelBy TopicLearning Paths
510 Intermediate

Closure Currying

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Currying" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Currying (named after Haskell Curry, formalised by SchΓΆnfinkel) is the theoretical foundation of lambda calculus β€” every multi-argument function can be expressed as nested single-argument functions. Key difference from OCaml: 1. **Implicit currying**: OCaml's `add x y` is syntactic sugar for `fun x

Tutorial

The Problem

Currying (named after Haskell Curry, formalised by SchΓΆnfinkel) is the theoretical foundation of lambda calculus β€” every multi-argument function can be expressed as nested single-argument functions. Practical benefits: unified partial application syntax (just call with fewer arguments), function composition works naturally on single-argument functions, and point-free style becomes possible. OCaml and Haskell have currying built in; Rust requires explicit nested closures, but the pattern is expressible and useful.

🎯 Learning Outcomes

  • β€’ Write curried functions returning impl Fn(i32) -> i32 from fn add(x: i32) -> impl Fn(i32) -> i32
  • β€’ Implement three-argument currying with nested Box<dyn Fn>
  • β€’ Write a generic curry<F> that converts an uncurried 2-arg function to curried form
  • β€’ Write uncurry that converts curried form back to a 2-arg function
  • β€’ Implement flip that reverses the argument order of a curried function
  • Code Example

    // Explicit currying via nested closures
    fn add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    let add5 = add(5);
    
    fn curry<A, B, C, F>(f: F) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>

    Key Differences

  • Implicit currying: OCaml's add x y is syntactic sugar for fun x -> (fun y -> x + y) β€” partial application is free. Rust's add(x, y) is a true 2-arg function requiring explicit closure wrapping.
  • **Box<dyn Fn> overhead**: Rust's generic curry must use Box<dyn Fn> to erase the return type of the nested closure; OCaml's type system handles this transparently.
  • **Copy constraints**: Rust's curry requires A: Copy, B: Copy to capture arguments in nested closures; OCaml copies integers implicitly.
  • **flip ergonomics**: Rust's flip requires complex lifetime/trait bounds; OCaml's let flip f x y = f y x is trivial.
  • OCaml Approach

    OCaml functions are curried by default:

    let add x y = x + y   (* add : int -> int -> int β€” already curried *)
    let add5 = add 5       (* partial application β€” no extra syntax *)
    let () = assert (add5 3 = 8)
    
    (* Three-arg currying *)
    let clamp lo hi x = max lo (min hi x)
    let clamp_0_100 = clamp 0 100
    
    (* Flip *)
    let flip f x y = f y x
    let sub_from_10 = flip (-) 10   (* fun x -> 10 - x *)
    

    OCaml's default currying makes all of these patterns natural without any boilerplate.

    Full Source

    #![allow(clippy::all)]
    //! Currying Pattern
    //!
    //! Explicit currying via nested closures returning closures.
    
    /// Curried add: add(x)(y) = x + y
    pub fn add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    
    /// Curried multiply: mul(x)(y) = x * y
    pub fn mul(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x * y
    }
    
    /// Three-argument curried clamp: clamp(lo)(hi)(x)
    pub fn clamp(lo: i32) -> Box<dyn Fn(i32) -> Box<dyn Fn(i32) -> i32>> {
        Box::new(move |hi| Box::new(move |x| x.max(lo).min(hi)))
    }
    
    /// Convert a 2-arg uncurried function to curried form.
    pub fn curry<A: Copy + 'static, B: Copy + 'static, C: 'static, F>(
        f: F,
    ) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>
    where
        F: Fn(A, B) -> C + Copy + 'static,
    {
        Box::new(move |a| Box::new(move |b| f(a, b)))
    }
    
    /// Convert a curried function back to uncurried.
    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)
    }
    
    /// Flip argument order for a curried function.
    pub fn flip<A: 'static, B: Copy + 'static, C: 'static, F, G>(
        f: F,
    ) -> Box<dyn Fn(B) -> Box<dyn Fn(A) -> C>>
    where
        F: Fn(A) -> G + Clone + 'static,
        G: Fn(B) -> C + 'static,
    {
        Box::new(move |b| {
            let f = f.clone();
            Box::new(move |a| f(a)(b))
        })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_curried_add() {
            assert_eq!(add(5)(3), 8);
            let add10 = add(10);
            assert_eq!(add10(0), 10);
            assert_eq!(add10(5), 15);
        }
    
        #[test]
        fn test_curried_mul() {
            assert_eq!(mul(6)(7), 42);
            let times3 = mul(3);
            assert_eq!(times3(4), 12);
        }
    
        #[test]
        fn test_curried_clamp() {
            let clamp_5_10 = clamp(5)(10);
            assert_eq!(clamp_5_10(7), 7);
            assert_eq!(clamp_5_10(2), 5);
            assert_eq!(clamp_5_10(15), 10);
        }
    
        #[test]
        fn test_curry() {
            let f = curry(|x: i32, y: i32| x * y);
            assert_eq!(f(6)(7), 42);
        }
    
        #[test]
        fn test_uncurry() {
            let g = uncurry(add);
            assert_eq!(g(3, 4), 7);
        }
    
        #[test]
        fn test_partial_application() {
            let add5 = add(5);
            let result: Vec<i32> = [1, 2, 3, 4, 5].iter().map(|&x| add5(x)).collect();
            assert_eq!(result, vec![6, 7, 8, 9, 10]);
        }
    
        #[test]
        fn test_curried_chain() {
            // Chain curried calls
            assert_eq!(add(add(1)(2))(3), 6); // (1+2)+3
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_curried_add() {
            assert_eq!(add(5)(3), 8);
            let add10 = add(10);
            assert_eq!(add10(0), 10);
            assert_eq!(add10(5), 15);
        }
    
        #[test]
        fn test_curried_mul() {
            assert_eq!(mul(6)(7), 42);
            let times3 = mul(3);
            assert_eq!(times3(4), 12);
        }
    
        #[test]
        fn test_curried_clamp() {
            let clamp_5_10 = clamp(5)(10);
            assert_eq!(clamp_5_10(7), 7);
            assert_eq!(clamp_5_10(2), 5);
            assert_eq!(clamp_5_10(15), 10);
        }
    
        #[test]
        fn test_curry() {
            let f = curry(|x: i32, y: i32| x * y);
            assert_eq!(f(6)(7), 42);
        }
    
        #[test]
        fn test_uncurry() {
            let g = uncurry(add);
            assert_eq!(g(3, 4), 7);
        }
    
        #[test]
        fn test_partial_application() {
            let add5 = add(5);
            let result: Vec<i32> = [1, 2, 3, 4, 5].iter().map(|&x| add5(x)).collect();
            assert_eq!(result, vec![6, 7, 8, 9, 10]);
        }
    
        #[test]
        fn test_curried_chain() {
            // Chain curried calls
            assert_eq!(add(add(1)(2))(3), 6); // (1+2)+3
        }
    }

    Deep Comparison

    OCaml vs Rust: Currying

    OCaml

    (* All functions are curried by default *)
    let add x y = x + y
    let add5 = add 5  (* automatic partial application *)
    
    let curry f x y = f (x, y)    (* tuple to curried *)
    let uncurry f (x, y) = f x y  (* curried to tuple *)
    

    Rust

    // Explicit currying via nested closures
    fn add(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    let add5 = add(5);
    
    fn curry<A, B, C, F>(f: F) -> Box<dyn Fn(A) -> Box<dyn Fn(B) -> C>>
    

    Key Differences

  • OCaml: All multi-arg functions are curried by default
  • Rust: Must explicitly return closures for currying
  • OCaml: let f x y = ... is automatically f: a -> b -> c
  • Rust: Need Box<dyn Fn> for nested impl Fn returns
  • Both support curry/uncurry conversion utilities
  • Exercises

  • Point-free style: Implement sum_of_squares: Vec<i32> -> i32 using only map, fold, and curried add/mul β€” no explicit lambda bodies.
  • Curry3: Implement curry3 that converts Fn(A, B, C) -> D to Fn(A) -> (Fn(B) -> (Fn(C) -> D)).
  • Practical currying: Use curried add, mul, and partial to build a tax_calculator(rate: f64)(price: f64) -> f64 and a discounted_tax(discount: f64)(rate: f64)(price: f64) -> f64.
  • Open Source Repos