Example 001: Applying a Function Twice
Difficulty: โญ Category: [Higher-Order Functions | Functional Composition | Currying] OCaml Source: Classic functional programming patternProblem Statement
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
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.
Rust Approach
Rust requires explicit handling of function types through: 1. Closures with trait bounds โ `impl Fn(T) -> T` captures any callable type 2. Function pointers โ `fn(T) -> T` for concrete function references 3. 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
Key Differences
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. 2. 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. 3. Generic Flexibility: Rust's `impl Fn(T) -> T` trait bound is more flexible than a bare function pointer, accepting closures with captured state. 4. Composition as a First-Class Pattern: Rust's `twice_compose` returns a closure, enabling functional composition patterns. OCaml achieves this naturally through function composition.Exercises
1. Implement `thrice` (apply a function three times) in both languages 2. Create a composition operator `|>` in Rust that mimics OCaml's `|>` pipeline 3. Implement a generic `apply_n_times` function that applies a function `n` times/// 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);
}
}
fn main() {
// Using function pointers
let quad = |x| twice(double, x);
let fourth = |x| twice(square, x);
println!("quad 3 = {}", quad(3)); // 12
println!("fourth 2 = {}", fourth(2)); // 16
// Using closure-based composition
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
}
/* Output:
quad 3 = 12
fourth 2 = 16
quad_composed 3 = 12
fourth_composed 2 = 16
*/
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 *)
๐ Detailed Comparison
OCaml vs Rust: Applying a Function Twice
Side-by-Side Code
OCaml
๐ช Show OCaml equivalent
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
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Function type | `('a -> 'a) -> 'a -> 'a` | `fn twice<T>(f: impl Fn(T) -> T, x: T) -> T` | ||
| Partial application | `let 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 closure | Implicit via type inference | Explicit via `impl Fn(T) -> T` or named `Box<dyn Fn>` |
Key Insights
1. 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.
2. 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.
3. 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.
4. 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
5. 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.