ExamplesBy LevelBy TopicLearning Paths
508 Intermediate

Closure Partial Application

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Partial Application" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. A function `add(x, y)` needs to be passed to `map` which expects `Fn(i32) -> i32`. Key difference from OCaml: 1. **Implicit vs. explicit**: OCaml has built

Tutorial

The Problem

A function add(x, y) needs to be passed to map which expects Fn(i32) -> i32. Partial application solves this: partial(add, 5) returns |y| add(5, y) — the 5 is baked in. This pattern appears everywhere: creating specialised predicates (between(0, 100, x)), building pipeline stages with fixed configuration, and adapting multi-argument functions to single-argument interfaces. Haskell and OCaml have partial application built into the language via currying; Rust requires explicit closures.

🎯 Learning Outcomes

  • • Implement generic partial that fixes the first argument of a 2-arg function
  • • Implement partial2 that fixes the first two arguments of a 3-arg function
  • • Understand A: Copy bound — the fixed argument is copied into each returned closure
  • • Write manual partial application with create_adder(n) and create_range_checker(lo, hi)
  • • Recognise the connection between partial application and currying
  • Code Example

    fn add(x: i32, y: i32) -> i32 { x + y }
    let add5 = |y| add(5, y);  // explicit closure required
    let clamp_0_100 = |x| clamp(0, 100, x);

    Key Differences

  • Implicit vs. explicit: OCaml has built-in partial application via currying — add 5 is legal. Rust requires an explicit closure or partial helper.
  • **Copy constraint**: Rust's partial requires A: Copy so the fixed argument is copied into each returned closure; OCaml copies int/float primitives implicitly.
  • **partial2 generality**: Rust's partial2 must be defined separately from partial; OCaml's currying handles any number of fixed arguments uniformly.
  • Specialisation at compile time: Rust's partial is monomorphised — the returned closure is a distinct type per call; OCaml uses uniform representation for all closures.
  • OCaml Approach

    OCaml functions are curried by default — partial application is syntactic:

    let add x y = x + y
    let add5 = add 5         (* partial application — no extra syntax *)
    let () = assert (add5 10 = 15)
    
    let clamp lo hi x = max lo (min hi x)
    let clamp_to_100 = clamp 0 100  (* fix first two args *)
    

    This is a fundamental difference: OCaml's multi-argument functions are syntactic sugar for nested single-argument functions; Rust's multi-argument functions are genuinely multi-argument.

    Full Source

    #![allow(clippy::all)]
    //! Partial Application with Closures
    //!
    //! Fix some arguments of a function, producing a specialized version.
    
    /// Add two numbers.
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }
    
    /// Clamp value into [lo, hi] range.
    pub fn clamp(lo: i32, hi: i32, x: i32) -> i32 {
        x.max(lo).min(hi)
    }
    
    /// Check if x is in [lo, hi] inclusive.
    pub fn between(lo: i32, hi: i32, x: i32) -> bool {
        x >= lo && x <= hi
    }
    
    /// Generic partial application: fix first argument of a 2-arg function.
    pub fn partial<A: Copy, B, C, F>(f: F, a: A) -> impl Fn(B) -> C
    where
        F: Fn(A, B) -> C,
    {
        move |b| f(a, b)
    }
    
    /// Partial with two fixed args (fix first two of a 3-arg function).
    pub fn partial2<A: Copy, B: Copy, C, D, F>(f: F, a: A, b: B) -> impl Fn(C) -> D
    where
        F: Fn(A, B, C) -> D,
    {
        move |c| f(a, b, c)
    }
    
    /// Manual closure-based partial application approach.
    pub fn create_adder(n: i32) -> impl Fn(i32) -> i32 {
        move |x| add(n, x)
    }
    
    /// Create a range checker with fixed bounds.
    pub fn create_range_checker(lo: i32, hi: i32) -> impl Fn(i32) -> bool {
        move |x| between(lo, hi, x)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_manual_partial_add() {
            let add5 = |y: i32| add(5, y);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_manual_partial_clamp() {
            let clamp_0_100 = |x| clamp(0, 100, x);
            assert_eq!(clamp_0_100(50), 50);
            assert_eq!(clamp_0_100(150), 100);
            assert_eq!(clamp_0_100(-10), 0);
        }
    
        #[test]
        fn test_generic_partial() {
            let mul_by_7 = partial(|x: i32, y: i32| x * y, 7);
            assert_eq!(mul_by_7(6), 42);
            assert_eq!(mul_by_7(0), 0);
        }
    
        #[test]
        fn test_partial2() {
            let check = partial2(between, 5, 10);
            assert!(check(7));
            assert!(check(5));
            assert!(check(10));
            assert!(!check(4));
            assert!(!check(11));
        }
    
        #[test]
        fn test_partial_string() {
            let prefix_checker = partial(|p: &str, s: &str| s.starts_with(p), "rust");
            assert!(prefix_checker("rustacean"));
            assert!(!prefix_checker("python"));
        }
    
        #[test]
        fn test_create_adder() {
            let add10 = create_adder(10);
            assert_eq!(add10(5), 15);
            assert_eq!(add10(-10), 0);
        }
    
        #[test]
        fn test_create_range_checker() {
            let in_teens = create_range_checker(13, 19);
            assert!(in_teens(15));
            assert!(in_teens(13));
            assert!(in_teens(19));
            assert!(!in_teens(12));
            assert!(!in_teens(20));
        }
    
        #[test]
        fn test_pipeline_with_partial() {
            let add5 = |x: &i32| add(5, *x);
            let double = |x: i32| x * 2;
            let in_teens = |x: &i32| between(13, 19, *x);
    
            let result: Vec<i32> = [1, 2, 3, 4, 5]
                .iter()
                .map(add5)
                .map(double)
                .filter(in_teens)
                .collect();
    
            assert_eq!(result, vec![14, 16, 18]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_manual_partial_add() {
            let add5 = |y: i32| add(5, y);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_manual_partial_clamp() {
            let clamp_0_100 = |x| clamp(0, 100, x);
            assert_eq!(clamp_0_100(50), 50);
            assert_eq!(clamp_0_100(150), 100);
            assert_eq!(clamp_0_100(-10), 0);
        }
    
        #[test]
        fn test_generic_partial() {
            let mul_by_7 = partial(|x: i32, y: i32| x * y, 7);
            assert_eq!(mul_by_7(6), 42);
            assert_eq!(mul_by_7(0), 0);
        }
    
        #[test]
        fn test_partial2() {
            let check = partial2(between, 5, 10);
            assert!(check(7));
            assert!(check(5));
            assert!(check(10));
            assert!(!check(4));
            assert!(!check(11));
        }
    
        #[test]
        fn test_partial_string() {
            let prefix_checker = partial(|p: &str, s: &str| s.starts_with(p), "rust");
            assert!(prefix_checker("rustacean"));
            assert!(!prefix_checker("python"));
        }
    
        #[test]
        fn test_create_adder() {
            let add10 = create_adder(10);
            assert_eq!(add10(5), 15);
            assert_eq!(add10(-10), 0);
        }
    
        #[test]
        fn test_create_range_checker() {
            let in_teens = create_range_checker(13, 19);
            assert!(in_teens(15));
            assert!(in_teens(13));
            assert!(in_teens(19));
            assert!(!in_teens(12));
            assert!(!in_teens(20));
        }
    
        #[test]
        fn test_pipeline_with_partial() {
            let add5 = |x: &i32| add(5, *x);
            let double = |x: i32| x * 2;
            let in_teens = |x: &i32| between(13, 19, *x);
    
            let result: Vec<i32> = [1, 2, 3, 4, 5]
                .iter()
                .map(add5)
                .map(double)
                .filter(in_teens)
                .collect();
    
            assert_eq!(result, vec![14, 16, 18]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Partial Application

    OCaml

    (* Functions are curried by default *)
    let add x y = x + y
    let add5 = add 5       (* automatic partial application *)
    let clamp lo hi x = max lo (min hi x)
    let clamp_0_100 = clamp 0 100
    

    Rust

    fn add(x: i32, y: i32) -> i32 { x + y }
    let add5 = |y| add(5, y);  // explicit closure required
    let clamp_0_100 = |x| clamp(0, 100, x);
    

    Key Differences

  • OCaml: Curried by default — partial application is automatic
  • Rust: Requires explicit closure to capture fixed arguments
  • OCaml: let f = g arg1 creates partial
  • Rust: let f = move |rest| g(arg1, rest) creates partial
  • Both integrate cleanly with map/filter pipelines
  • Exercises

  • **partial_right**: Write fn partial_right<A, B: Copy, C, F>(f: F, b: B) -> impl Fn(A) -> C where F: Fn(A, B) -> C that fixes the second argument.
  • URL builder: Use partial or manual closures to build get_users, get_orders from a generic make_request(method, endpoint) base function.
  • Pipeline with partial: Build a numeric pipeline [partial(clamp, 0), partial(|x,n| x*n, 2), |x| x as f64 / 100.0] using chain_closures from example 505 and verify the output.
  • Open Source Repos