ExamplesBy LevelBy TopicLearning Paths
074 Intermediate

074 — Currying and Partial Application (Applied)

Functional Programming

Tutorial

The Problem

This example applies currying and partial application to practical patterns — building factories, greeting generators, and transformation pipelines. Where example 005 introduced the theory, this example shows the patterns in context: make_adder(5) returns a reusable function, apply_twice demonstrates function composition from first principles.

Partial application is ubiquitous in functional programming: event handler factories in UIs, middleware pipelines in web frameworks, predicate factories in query builders, and configuration-bound operations. Understanding how to build and compose these in Rust is essential for ergonomic API design.

🎯 Learning Outcomes

  • • Build function factories with make_adder(n) -> impl Fn(i32) -> i32
  • • Use closures for multi-level currying: function returning function returning value
  • • Apply apply_twice to demonstrate functions as values
  • • Use compose to build pipelines
  • • Understand the move keyword for capturing environment in closures
  • Code Example

    #![allow(clippy::all)]
    // 074: Currying and Partial Application
    
    // Approach 1: Closures for partial application
    fn add(x: i32, y: i32) -> i32 {
        x + y
    }
    fn multiply(x: i32, y: i32) -> i32 {
        x * y
    }
    
    fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    
    fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x * y
    }
    
    // Approach 2: Curried function returning closures
    fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String + '_> + '_ {
        move |name: &str| {
            let owned_prefix = prefix.to_string();
            let owned_name = name.to_string();
            Box::new(move |suffix: &str| format!("{} {}{}", owned_prefix, owned_name, suffix))
        }
    }
    
    // Simpler version:
    fn greet(prefix: &str, name: &str, suffix: &str) -> String {
        format!("{} {}{}", prefix, name, suffix)
    }
    
    // Approach 3: Higher-order + partial application
    fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
        f(f(x))
    }
    
    fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
        move |x| f(g(x))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partial_application() {
            let add5 = make_adder(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
    
            let double = make_multiplier(2);
            assert_eq!(double(7), 14);
    
            let triple = make_multiplier(3);
            assert_eq!(triple(4), 12);
        }
    
        #[test]
        fn test_apply_twice() {
            let add5 = make_adder(5);
            assert_eq!(apply_twice(&add5, 0), 10);
            assert_eq!(apply_twice(&add5, 5), 15);
    
            let double = make_multiplier(2);
            assert_eq!(apply_twice(&double, 3), 12);
        }
    
        #[test]
        fn test_compose() {
            let add5_then_double = compose(make_multiplier(2), make_adder(5));
            assert_eq!(add5_then_double(3), 16); // (3+5)*2
    
            let double_then_add5 = compose(make_adder(5), make_multiplier(2));
            assert_eq!(double_then_add5(3), 11); // 3*2+5
        }
    
        #[test]
        fn test_greet() {
            assert_eq!(greet("Hello", "World", "!"), "Hello World!");
        }
    }

    Key Differences

  • **move requirement**: Rust requires move in the closure to capture x by value. OCaml captures by closure environment automatically (GC-managed). Without move, Rust would borrow x — problematic when the closure outlives the function.
  • **impl Fn return type**: Rust's -> impl Fn(i32) -> i32 return type is required because closures have unique anonymous types. OCaml's fun x -> ... return type is inferred as int -> int.
  • Lifetime: The returned closure borrows nothing from the outer scope (because of move), so it has 'static lifetime. OCaml closures can safely reference the outer scope due to GC.
  • Higher-order composition: compose(f, g) in Rust requires + 'static bounds if stored. OCaml's fun x -> f (g x) composes naturally.
  • OCaml Approach

    OCaml functions are automatically curried, so partial application is natural:

    let make_adder x y = x + y   (* equivalent to: let make_adder x = fun y -> x + y *)
    let add5 = make_adder 5       (* partial application: bind x=5 *)
    let _ = add5 3                (* evaluates to 8 *)
    
    let apply_twice f x = f (f x)
    let compose f g = fun x -> f (g x)
    let double_then_add5 = compose add5 (fun x -> x * 2)
    

    OCaml's implicit currying means every multi-argument function is already a curried function. Partial application is just function application with fewer arguments than the full arity.

    Full Source

    #![allow(clippy::all)]
    // 074: Currying and Partial Application
    
    // Approach 1: Closures for partial application
    fn add(x: i32, y: i32) -> i32 {
        x + y
    }
    fn multiply(x: i32, y: i32) -> i32 {
        x * y
    }
    
    fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x + y
    }
    
    fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
        move |y| x * y
    }
    
    // Approach 2: Curried function returning closures
    fn make_greeting(prefix: &str) -> impl Fn(&str) -> Box<dyn Fn(&str) -> String + '_> + '_ {
        move |name: &str| {
            let owned_prefix = prefix.to_string();
            let owned_name = name.to_string();
            Box::new(move |suffix: &str| format!("{} {}{}", owned_prefix, owned_name, suffix))
        }
    }
    
    // Simpler version:
    fn greet(prefix: &str, name: &str, suffix: &str) -> String {
        format!("{} {}{}", prefix, name, suffix)
    }
    
    // Approach 3: Higher-order + partial application
    fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
        f(f(x))
    }
    
    fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
        move |x| f(g(x))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partial_application() {
            let add5 = make_adder(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
    
            let double = make_multiplier(2);
            assert_eq!(double(7), 14);
    
            let triple = make_multiplier(3);
            assert_eq!(triple(4), 12);
        }
    
        #[test]
        fn test_apply_twice() {
            let add5 = make_adder(5);
            assert_eq!(apply_twice(&add5, 0), 10);
            assert_eq!(apply_twice(&add5, 5), 15);
    
            let double = make_multiplier(2);
            assert_eq!(apply_twice(&double, 3), 12);
        }
    
        #[test]
        fn test_compose() {
            let add5_then_double = compose(make_multiplier(2), make_adder(5));
            assert_eq!(add5_then_double(3), 16); // (3+5)*2
    
            let double_then_add5 = compose(make_adder(5), make_multiplier(2));
            assert_eq!(double_then_add5(3), 11); // 3*2+5
        }
    
        #[test]
        fn test_greet() {
            assert_eq!(greet("Hello", "World", "!"), "Hello World!");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partial_application() {
            let add5 = make_adder(5);
            assert_eq!(add5(3), 8);
            assert_eq!(add5(0), 5);
    
            let double = make_multiplier(2);
            assert_eq!(double(7), 14);
    
            let triple = make_multiplier(3);
            assert_eq!(triple(4), 12);
        }
    
        #[test]
        fn test_apply_twice() {
            let add5 = make_adder(5);
            assert_eq!(apply_twice(&add5, 0), 10);
            assert_eq!(apply_twice(&add5, 5), 15);
    
            let double = make_multiplier(2);
            assert_eq!(apply_twice(&double, 3), 12);
        }
    
        #[test]
        fn test_compose() {
            let add5_then_double = compose(make_multiplier(2), make_adder(5));
            assert_eq!(add5_then_double(3), 16); // (3+5)*2
    
            let double_then_add5 = compose(make_adder(5), make_multiplier(2));
            assert_eq!(double_then_add5(3), 11); // 3*2+5
        }
    
        #[test]
        fn test_greet() {
            assert_eq!(greet("Hello", "World", "!"), "Hello World!");
        }
    }

    Deep Comparison

    Core Insight

    In OCaml, let add x y = x + y is actually let add = fun x -> fun y -> x + y. Partial application (add 5) returns a function. Rust functions aren't curried — you use closures to simulate partial application.

    OCaml Approach

  • • All functions are automatically curried
  • let add x y = x + yadd 5 returns fun y -> 5 + y
  • • Free partial application of any prefix of arguments
  • Rust Approach

  • • Functions are NOT curried
  • • Closures capture variables: let add5 = |y| 5 + y;
  • move closures for ownership transfer
  • • Can return closures with impl Fn(T) -> U
  • Comparison Table

    FeatureOCamlRust
    Curried by defaultYesNo
    Partial applicationf x (give fewer args)Closure capturing
    Return functionAutomaticimpl Fn(T) -> U
    Closure syntaxfun x -> ...\|x\| ...

    Exercises

  • Tax calculator: Write make_tax_calculator(rate: f64) -> impl Fn(f64) -> f64 and make_discount(pct: f64) -> impl Fn(f64) -> f64. Compose them into a calculate_price pipeline.
  • Memoized factory: Write memoized_make_adder(cache: &mut HashMap<i32, Box<dyn Fn(i32) -> i32>>, n: i32) -> &Box<dyn Fn(i32) -> i32> that caches the created adder function.
  • Pipeline DSL: Using compose, build a pipeline for data transformation: trim -> lowercase -> split_words -> filter_short_words -> join_with_comma. Each step is a partial application of a generic combinator.
  • Open Source Repos