ExamplesBy LevelBy TopicLearning Paths
001 Intermediate

Applying a Function Twice

Higher-Order FunctionsFunctional CompositionCurrying

Tutorial Video

Text description (accessibility)

This video demonstrates the "Applying a Function Twice" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Higher-Order Functions, Functional Composition, Currying. Given a function `f` and a value `x`, apply the function `f` to `x`, and then apply `f` again to the result. Key difference from OCaml: 1. **Currying:** OCaml's automatic currying means `twice double` is syntactic sugar for `(twice double)`. In Rust, we write `|x| twice(double, x)` or use `twice_compose` to achieve similar partial application.

Tutorial

The Problem

Given a function f and a value x, apply the function f to x, and then apply f again to the result. This is a fundamental pattern in functional programming that demonstrates how functions can be treated as first-class values.

🎯 Learning Outcomes

  • • Understand higher-order functions — functions that take other functions as arguments
  • • Master generic function types in Rust using trait bounds and function pointers
  • • Learn the difference between closures and function pointers for passing functions
  • • Explore function composition as a way to build reusable transformations
  • 🦀 The Rust Way

    Rust requires explicit handling of function types through:

  • Closures with trait boundsimpl Fn(T) -> T captures any callable type
  • Function pointersfn(T) -> T for concrete function references
  • Closure composition — returning closures that capture the original function
  • We provide three implementations that grow in expressiveness:

  • • **twice** — accepts any callable (closure or function) via trait bound
  • • **twice_fn** — accepts explicit function pointers
  • • **twice_compose** — returns a new closure that can be stored and reused
  • Code Example

    pub fn twice<T>(f: impl Fn(T) -> T, x: T) -> T {
        f(f(x))
    }
    
    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn square(x: i32) -> i32 {
        x * x
    }
    
    fn main() {
        let quad = |x| twice(double, x);
        let fourth = |x| twice(square, x);
    
        println!("quad 3   = {}", quad(3));     // 12
        println!("fourth 2 = {}", fourth(2));   // 16
    }

    Key Differences

  • Currying: OCaml's automatic currying means twice double is syntactic sugar for (twice double). In Rust, we write |x| twice(double, x) or use twice_compose to achieve similar partial application.
  • Function Types: OCaml treats all functions uniformly with type 'a -> 'b. Rust distinguishes between concrete function pointers fn(T) -> T and closures captured via impl Fn(T) -> T, giving more control but requiring explicit choices.
  • Generic Flexibility: Rust's impl Fn(T) -> T trait bound is more flexible than a bare function pointer, accepting closures with captured state.
  • Composition as a First-Class Pattern: Rust's twice_compose returns a closure, enabling functional composition patterns. OCaml achieves this naturally through function composition.
  • OCaml Approach

    OCaml's twice function is elegantly simple:

    let twice f x = f (f x)
    

    This takes advantage of OCaml's currying: f and x are separate parameters, and partial application is implicit. You can write let quad = twice double to bind double as the function argument, creating a partially-applied function.

    Full Source

    #![allow(clippy::all)]
    /// Apply a function twice to a value.
    /// Demonstrates higher-order functions and partial application.
    ///
    /// Takes a function `f` and a value `x`, applies `f` to `x`, then applies `f` again.
    pub fn twice<T>(f: impl Fn(T) -> T, x: T) -> T {
        f(f(x))
    }
    
    /// Alternative using function pointers (more explicit type).
    /// Useful when you need to pass function pointers directly.
    pub fn twice_fn<T: Copy>(f: fn(T) -> T, x: T) -> T {
        f(f(x))
    }
    
    /// Functional approach: compose a function with itself.
    /// Returns a closure that applies the original function twice.
    pub fn twice_compose<T: 'static>(f: impl Fn(T) -> T + 'static) -> impl Fn(T) -> T {
        move |x| f(f(x))
    }
    
    // Helper functions for examples
    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_twice_double_zero() {
            assert_eq!(twice(double, 0), 0);
        }
    
        #[test]
        fn test_twice_double_positive() {
            // double(3) = 6, double(6) = 12
            assert_eq!(twice(double, 3), 12);
        }
    
        #[test]
        fn test_twice_square_small() {
            // square(2) = 4, square(4) = 16
            assert_eq!(twice(square, 2), 16);
        }
    
        #[test]
        fn test_twice_square_one() {
            assert_eq!(twice(square, 1), 1);
        }
    
        #[test]
        fn test_twice_fn_double() {
            assert_eq!(twice_fn(double, 3), 12);
        }
    
        #[test]
        fn test_twice_fn_square() {
            assert_eq!(twice_fn(square, 2), 16);
        }
    
        #[test]
        fn test_twice_compose_double() {
            let quad = twice_compose(double);
            assert_eq!(quad(3), 12);
        }
    
        #[test]
        fn test_twice_compose_square() {
            let fourth = twice_compose(square);
            assert_eq!(fourth(2), 16);
        }
    
        #[test]
        fn test_twice_with_closure() {
            let add_five = |x| x + 5;
            assert_eq!(twice(add_five, 0), 10);
        }
    
        #[test]
        fn test_twice_negative() {
            assert_eq!(twice(double, -3), -12);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_twice_double_zero() {
            assert_eq!(twice(double, 0), 0);
        }
    
        #[test]
        fn test_twice_double_positive() {
            // double(3) = 6, double(6) = 12
            assert_eq!(twice(double, 3), 12);
        }
    
        #[test]
        fn test_twice_square_small() {
            // square(2) = 4, square(4) = 16
            assert_eq!(twice(square, 2), 16);
        }
    
        #[test]
        fn test_twice_square_one() {
            assert_eq!(twice(square, 1), 1);
        }
    
        #[test]
        fn test_twice_fn_double() {
            assert_eq!(twice_fn(double, 3), 12);
        }
    
        #[test]
        fn test_twice_fn_square() {
            assert_eq!(twice_fn(square, 2), 16);
        }
    
        #[test]
        fn test_twice_compose_double() {
            let quad = twice_compose(double);
            assert_eq!(quad(3), 12);
        }
    
        #[test]
        fn test_twice_compose_square() {
            let fourth = twice_compose(square);
            assert_eq!(fourth(2), 16);
        }
    
        #[test]
        fn test_twice_with_closure() {
            let add_five = |x| x + 5;
            assert_eq!(twice(add_five, 0), 10);
        }
    
        #[test]
        fn test_twice_negative() {
            assert_eq!(twice(double, -3), -12);
        }
    }

    Deep Comparison

    OCaml vs Rust: Applying a Function Twice

    Side-by-Side Code

    OCaml

    let twice f x = f (f x)
    
    let double x = 2 * x
    let square x = x * x
    
    let quad   = twice double   (* applies double twice *)
    let fourth = twice square   (* applies square twice *)
    
    let () =
      Printf.printf "quad 3   = %d\n" (quad 3);    (* 12 *)
      Printf.printf "fourth 2 = %d\n" (fourth 2)   (* 16 *)
    

    Rust (idiomatic — using closures and trait bounds)

    pub fn twice<T>(f: impl Fn(T) -> T, x: T) -> T {
        f(f(x))
    }
    
    pub fn double(x: i32) -> i32 {
        2 * x
    }
    
    pub fn square(x: i32) -> i32 {
        x * x
    }
    
    fn main() {
        let quad = |x| twice(double, x);
        let fourth = |x| twice(square, x);
    
        println!("quad 3   = {}", quad(3));     // 12
        println!("fourth 2 = {}", fourth(2));   // 16
    }
    

    Rust (functional — composition-based)

    pub fn twice_compose<T: 'static>(f: impl Fn(T) -> T + 'static) -> impl Fn(T) -> T {
        move |x| f(f(x))
    }
    
    fn main() {
        let quad_composed = twice_compose(double);
        let fourth_composed = twice_compose(square);
    
        println!("quad_composed 3   = {}", quad_composed(3));     // 12
        println!("fourth_composed 2 = {}", fourth_composed(2));   // 16
    }
    

    Type Signatures

    ConceptOCamlRust
    Function type('a -> 'a) -> 'a -> 'afn twice<T>(f: impl Fn(T) -> T, x: T) -> T
    Partial applicationlet quad = twice double (automatic)\|x\| twice(double, x) (explicit closure)
    Closure type'a -> 'a (implicit)impl Fn(T) -> T (trait bound) or fn(T) -> T (function pointer)
    Returned closureImplicit via type inferenceExplicit via impl Fn(T) -> T or named Box<dyn Fn>

    Key Insights

  • Currying is explicit in Rust: OCaml's automatic currying treats twice double as a partial application. In Rust, we must explicitly create a closure |x| twice(double, x) to achieve the same effect. This reflects Rust's principle of making ownership and lifetimes explicit.
  • Function types are more flexible in Rust: The trait bound impl Fn(T) -> T accepts any callable (function pointer, closure with captured variables, method reference). OCaml lumps everything into function types but forces you to think about captures differently.
  • Lifetime and Move Semantics Matter: The twice_compose variant requires T: 'static when returning a closure because the closure needs to own the captured function. In OCaml, this complexity is hidden by the garbage collector.
  • Three approaches for different needs:
  • - twice with impl Fn — most flexible, works with any callable - twice_fn with function pointers — explicit, predictable, no allocations - twice_compose returning a closure — enables function composition chains

  • Ownership vs. Garbage Collection: OCaml lets you casually construct and return functions without thinking about lifetimes. Rust forces you to decide whether the closure owns or borrows the function, leading to safer code but more boilerplate.
  • When to Use Each Style

    Use idiomatic Rust when: You're building APIs that need to accept various callables (functions, closures, method references). The impl Fn bound is the Swiss Army knife of function passing.

    Use function pointers when: You need maximum performance and predictability (no allocations, no closures on the heap). Function pointers are zero-cost abstractions.

    **Use closure composition (twice_compose) when:** You're building functional programming libraries where composing functions is a core operation. This style mirrors OCaml idioms most closely.

    Performance Characteristics

  • • **OCaml twice:** Zero-cost abstraction; inlining works naturally.
  • • **Rust twice with impl Fn:** Zero-cost when the function is a concrete type (monomorphized), but the generic specialization happens at compile time.
  • • **Rust twice_fn:** Guaranteed zero-cost; function pointers are thin pointers with no captures.
  • • **Rust twice_compose returning closure:** May allocate if the closure is boxed or captured across function boundaries. If stack-allocated, still zero-cost.
  • The bottom line: Rust's generics are as efficient as OCaml's polymorphism, but require explicit attention to allocation and ownership.

    Exercises

  • Implement thrice (apply a function three times) in both languages
  • Create a composition operator |> in Rust that mimics OCaml's |> pipeline
  • Implement a generic apply_n_times function that applies a function n times
  • Open Source Repos