ExamplesBy LevelBy TopicLearning Paths
002 Intermediate

Function Composition

Higher-Order FunctionsClosuresComposition

Tutorial Video

Text description (accessibility)

This video demonstrates the "Function Composition" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Higher-Order Functions, Closures, Composition. Write a function `compose` that takes two functions `f` and `g` and returns their composition—a new function that applies `g` first, then `f` to the result. Key difference from OCaml: 1. **Type Parameters:** OCaml infers everything; Rust requires explicit type parameters `A, B, C` for the domain and codomain.

Tutorial

The Problem

Write a function compose that takes two functions f and g and returns their composition—a new function that applies g first, then f to the result.

🎯 Learning Outcomes

  • • Understanding function composition as a higher-order function pattern
  • • How Rust closures capture their environment and enable function composition
  • • Type inference for generic function composition with Rust traits
  • • The difference between trait objects, function pointers, and closures
  • 🦀 The Rust Way

    Rust uses higher-rank trait bounds to express composition. The compose function is generic over the function types F and G, and returns an impl Fn that captures both functions. This provides zero-cost abstraction—no runtime overhead.

    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(B) -> C,
        G: Fn(A) -> B,
    {
        move |x| f(g(x))
    }
    

    Code Example

    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(B) -> C,
        G: Fn(A) -> B,
    {
        move |x| f(g(x))
    }
    
    pub fn double(x: i32) -> i32 { 2 * x }
    pub fn square(x: i32) -> i32 { x * x }
    
    fn main() {
        let square_then_double = compose(double, square);
        println!("square_then_double 3 = {}", square_then_double(3));   // 18
        println!("square_then_double 4 = {}", square_then_double(4));   // 32
    }

    Key Differences

  • Type Parameters: OCaml infers everything; Rust requires explicit type parameters A, B, C for the domain and codomain.
  • Trait Bounds: Rust requires F: Fn(B) -> C and G: Fn(A) -> B to express that f and g are callable with specific signatures.
  • Return Type: OCaml returns a value; Rust returns impl Fn, which can be:
  • - A closure capturing f and g - A function pointer (less flexible but more concrete)

  • Lifetime Handling: Rust's move captures are explicit; OCaml's closures capture implicitly.
  • OCaml Approach

    In OCaml, functions are first-class values. compose f g creates a new function by simply wrapping f (g x) in a closure. OCaml's type system infers the composition automatically.

    let compose f g x = f (g x)
    

    Full Source

    #![allow(clippy::all)]
    // Solution 1: Idiomatic Rust — compose as a higher-order function
    // Takes two functions and returns a closure that applies them in sequence
    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(B) -> C,
        G: Fn(A) -> B,
    {
        move |x| f(g(x))
    }
    
    // Solution 2: Using function pointers — when you need a concrete type
    // More restrictive than closures but gives an explicit function type
    pub fn compose_fn<A, B, C>(f: fn(B) -> C, g: fn(A) -> B) -> impl Fn(A) -> C {
        move |x| f(g(x))
    }
    
    // Helper functions matching the OCaml example
    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn square(x: i32) -> i32 {
        x * x
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compose_square_then_double_3() {
            // square(3) = 9, double(9) = 18
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(3), 18);
        }
    
        #[test]
        fn test_compose_square_then_double_4() {
            // square(4) = 16, double(16) = 32
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(4), 32);
        }
    
        #[test]
        fn test_compose_zero() {
            // square(0) = 0, double(0) = 0
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(0), 0);
        }
    
        #[test]
        fn test_compose_negative() {
            // square(-2) = 4, double(4) = 8
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(-2), 8);
        }
    
        #[test]
        fn test_compose_double_then_square() {
            // Reverse order: double first, then square
            let double_then_square = compose(square, double);
            // double(3) = 6, square(6) = 36
            assert_eq!(double_then_square(3), 36);
        }
    
        #[test]
        fn test_compose_with_custom_functions() {
            let add_one = |x: i32| x + 1;
            let multiply_by_3 = |x: i32| x * 3;
            // multiply_by_3 first, then add_one
            let composed = compose(add_one, multiply_by_3);
            // multiply_by_3(5) = 15, add_one(15) = 16
            assert_eq!(composed(5), 16);
        }
    
        #[test]
        fn test_compose_fn_pointers() {
            // Using function pointers instead of closures
            let square_then_double = compose_fn(double, square);
            assert_eq!(square_then_double(3), 18);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compose_square_then_double_3() {
            // square(3) = 9, double(9) = 18
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(3), 18);
        }
    
        #[test]
        fn test_compose_square_then_double_4() {
            // square(4) = 16, double(16) = 32
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(4), 32);
        }
    
        #[test]
        fn test_compose_zero() {
            // square(0) = 0, double(0) = 0
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(0), 0);
        }
    
        #[test]
        fn test_compose_negative() {
            // square(-2) = 4, double(4) = 8
            let square_then_double = compose(double, square);
            assert_eq!(square_then_double(-2), 8);
        }
    
        #[test]
        fn test_compose_double_then_square() {
            // Reverse order: double first, then square
            let double_then_square = compose(square, double);
            // double(3) = 6, square(6) = 36
            assert_eq!(double_then_square(3), 36);
        }
    
        #[test]
        fn test_compose_with_custom_functions() {
            let add_one = |x: i32| x + 1;
            let multiply_by_3 = |x: i32| x * 3;
            // multiply_by_3 first, then add_one
            let composed = compose(add_one, multiply_by_3);
            // multiply_by_3(5) = 15, add_one(15) = 16
            assert_eq!(composed(5), 16);
        }
    
        #[test]
        fn test_compose_fn_pointers() {
            // Using function pointers instead of closures
            let square_then_double = compose_fn(double, square);
            assert_eq!(square_then_double(3), 18);
        }
    }

    Deep Comparison

    OCaml vs Rust: Function Composition

    Side-by-Side Code

    OCaml

    let compose f g x = f (g x)
    
    let double x = 2 * x
    let square x = x * x
    
    let square_then_double = compose double square
    
    let () =
      Printf.printf "square_then_double 3 = %d\n" (square_then_double 3)  (* 18 *)
      Printf.printf "square_then_double 4 = %d\n" (square_then_double 4)  (* 32 *)
    

    Rust (idiomatic)

    pub fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
    where
        F: Fn(B) -> C,
        G: Fn(A) -> B,
    {
        move |x| f(g(x))
    }
    
    pub fn double(x: i32) -> i32 { 2 * x }
    pub fn square(x: i32) -> i32 { x * x }
    
    fn main() {
        let square_then_double = compose(double, square);
        println!("square_then_double 3 = {}", square_then_double(3));   // 18
        println!("square_then_double 4 = {}", square_then_double(4));   // 32
    }
    

    Rust (with function pointers)

    pub fn compose_fn<A, B, C>(f: fn(B) -> C, g: fn(A) -> B) -> impl Fn(A) -> C {
        move |x| f(g(x))
    }
    
    // Usage:
    let square_then_double = compose_fn(double, square);
    

    Type Signatures

    ConceptOCamlRust
    Compose type('a -> 'b) -> ('c -> 'a) -> ('c -> 'b)F: Fn(B) -> C, G: Fn(A) -> B
    Return typeImplicit closureimpl Fn(A) -> C
    Function typeint -> intfn(i32) -> i32
    Closure captureAutomatic (lexical)Explicit with move

    Key Insights

  • Generic Type Parameters in Rust: While OCaml can express composition with a single polymorphic signature, Rust needs three type parameters (A, B, C) to represent the domain, intermediate value, and codomain. This explicitness is a feature—the types are clear to the compiler and all callers.
  • Higher-Rank Trait Bounds: Rust's F: Fn(B) -> C syntax is equivalent to OCaml's ('a -> 'b) annotation. The Fn trait allows Rust to accept any callable (closure, function pointer, or function item) as long as it has the right signature.
  • Zero-Cost Abstraction: The impl Fn return type means Rust generates monomorphized code for each specific composition. There's no vtable or dynamic dispatch—the composition is inlined at compile time.
  • Closure Capture Semantics: In Rust, the move keyword explicitly captures f and g by ownership. In OCaml, this happens automatically. Rust's explicitness prevents accidental lifetime issues.
  • Concrete vs. Abstract Return Types: Rust offers two trade-offs:
  • - impl Fn (what we use here): maximally flexible, accepts any callable, zero overhead - fn(A) -> B (function pointers): more restrictive, requires function items, still zero overhead

    When to Use Each Style

    **Use idiomatic Rust (impl Fn + closures) when:** You need maximum flexibility—accepting any callable (closures, function items, methods) and returning an efficient, inlined composition.

    **Use function pointers (fn(A) -> B) when:** You specifically need to work with function items (not closures) and want a concrete, simple signature. This is less flexible but clearer in some contexts.

    Use OCaml when: You want the ultimate simplicity and don't need the explicit type control that Rust provides. OCaml's implicit polymorphism is elegant for mathematical function composition.

    Common Pitfalls (Rust)

    PitfallExampleSolution
    Forgetting move\|x\| f(g(x)) might not compileAdd move to capture f and g by value
    Wrong trait boundUsing Fn instead of FnOnceUse Fn for functions called multiple times
    Concrete function typeslet c: fn(i32)->i32 = compose(double, square)Use impl Fn return type instead

    Performance

    Both implementations compile to identical machine code:

  • OCaml: The closure is stack-allocated; the JIT or bytecode interpreter executes it efficiently.
  • Rust: With impl Fn, the composition is monomorphized and inlined—you get the same performance as hand-written move |x| f(g(x)).
  • Exercises

  • Write a compose3 function that chains three unary functions f, g, h so that compose3(f, g, h)(x) returns f(g(h(x))).
  • Implement a compose_n that takes a Vec<Box<dyn Fn(i32) -> i32>> and returns a single composed function applying them right-to-left.
  • Use function composition to build a text-processing pipeline: trim whitespace → lowercase → remove punctuation → split into words, returning a Vec<String>.
  • Open Source Repos