🦀 Functional Rust

Example 002: Function Composition

Difficulty:Category: Higher-Order Functions | Closures | Composition OCaml Source: Functional Programming fundamentals

Problem Statement

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

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)

Rust Approach

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))
}

Key Differences

1. Type Parameters: OCaml infers everything; Rust requires explicit type parameters `A, B, C` for the domain and codomain. 2. Trait Bounds: Rust requires `F: Fn(B) -> C` and `G: Fn(A) -> B` to express that `f` and `g` are callable with specific signatures. 3. 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) 4. Lifetime Handling: Rust's `move` captures are explicit; OCaml's closures capture implicitly.
// 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
}

fn main() {
    let square_then_double = compose(double, square);
    println!("square_then_double 3 = {}", square_then_double(3));
    println!("square_then_double 4 = {}", square_then_double(4));

    let double_then_square = compose(square, double);
    println!("double_then_square 3 = {}", double_then_square(3));
}

/* Output:
   square_then_double 3 = 18
   square_then_double 4 = 32
   double_then_square 3 = 36
*/
let compose f g x = f (g x)

let double x = 2 * x
let square x = x * x

(* square first, then double *)
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 *)

📊 Detailed Comparison

OCaml vs Rust: Function Composition

Side-by-Side Code

OCaml

🐪 Show OCaml equivalent
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 closure`impl Fn(A) -> C`
Function type`int -> int``fn(i32) -> i32`
Closure captureAutomatic (lexical)Explicit with `move`

Key Insights

1. 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.

2. 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.

3. 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.

4. 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.

5. 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 `FnOnce`Use `Fn` for functions called multiple times
Concrete function types`let 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))`.