051 — Applying a Function Twice
Tutorial Video
Text description (accessibility)
This video demonstrates the "051 — Applying a Function Twice" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Applying a function twice — `f(f(x))` — is a simple but illuminating exercise in higher-order functions and partial application. Key difference from OCaml: 1. **Automatic currying**: OCaml's `twice f x` is automatically `twice f` (partial application). Rust needs `twice_partial(f)` as a separate function returning a closure.
Tutorial
The Problem
Applying a function twice — f(f(x)) — is a simple but illuminating exercise in higher-order functions and partial application. It demonstrates that functions are first-class values: twice takes a function as a parameter and returns a value (or a new function via partial application). This is the simplest non-trivial higher-order function, used as an introduction in OCaml's CS3110 course at Cornell.
The twice combinator generalizes to apply_n_times(f, n, x) which applies f exactly n times — the basis for iterative computation in functional languages, Church numerals (see example 098), and fixed-point iteration in numerical methods.
🎯 Learning Outcomes
twice(f, x) -> T and the partially applied twice_partial(f) -> Fn(T) -> Timpl Fn(T) -> T as a generic function typeFn, FnMut, and FnOnce for this use caseF: Fn(T) -> T (not FnOnce) as the trait bound since the function is called twice in twice(f, x)impl Fn(T) -> T from twice_partial(f) using move |x| f(f(x))Code Example
pub fn twice<T, F>(f: F, x: T) -> T
where
F: Fn(T) -> T,
{
f(f(x))
}
// Usage (both arguments supplied):
twice(double, 3) // 12
twice(square, 2) // 16Key Differences
twice f x is automatically twice f (partial application). Rust needs twice_partial(f) as a separate function returning a closure.Fn vs FnOnce**: Rust distinguishes single-use (FnOnce) and multi-use (Fn) closures. Calling f twice requires F: Fn(T) -> T. OCaml has no such distinction.f is called.move closure**: twice_partial captures f by move into the returned closure. The move keyword transfers ownership of f into the closure.Fn vs FnOnce:** twice requires F: Fn(T) -> T (not FnOnce) because it calls f twice. FnOnce can only be called once — using it here would cause a compile error on the second call.impl Fn:** twice_partial returns impl Fn(T) -> T. The concrete closure type is unnameable, so impl Fn is used. In a trait object context, Box<dyn Fn(T) -> T> would be needed.twice applied to itself computes 4 times: twice(twice, f) applies f four times. This is the basis of Church numerals: 0 = id, 1 = once, 2 = twice, n = apply_n.@@:** OCaml's function application operator @@ reads as f @@ x instead of f x. twice f @@ x applies twice f to x. Rust has no equivalent — use f(x) or the explicit pipe function.OCaml Approach
OCaml's version from CS3110: let twice f x = f (f x). This is automatically curried: let quad = twice double — partially applies twice with double, producing a function int -> int that quadruples its argument. let apply_n_times f n x = if n = 0 then x else apply_n_times f (n-1) (f x) generalizes to n applications.
Full Source
#![allow(clippy::all)]
//! # Applying a Function Twice
//! OCaml CS3110 — Higher-order function that applies a function twice,
//! demonstrating currying and partial application.
// ── Implementation 1: Idiomatic Rust ────────────────────────────────────────
//
// Generic over the argument type T and any function F that maps T → T.
// `F: Fn(T) -> T` means f can be called repeatedly (shared reference semantics).
// Ownership: x is moved in, the intermediate result of f(x) is moved into f(…).
/// Apply `f` to `x` twice: `f(f(x))`.
pub fn twice<T, F>(f: F, x: T) -> T
where
F: Fn(T) -> T,
{
f(f(x))
}
// ── Implementation 2: Partial application (curried style) ────────────────────
//
// Returns a closure that captures `f` by move.
// This matches OCaml's `let quad = twice double` — bind the function,
// get back a new function waiting for the argument.
//
// `impl Fn(T) -> T` in return position: the compiler knows the concrete
// closure type; we hide it behind `impl Trait` so callers stay generic.
/// Partially apply `twice`: bind `f`, return a new `Fn(T) -> T`.
///
/// Example: `let quad = twice_partial(double);` — then `quad(3) == 12`.
pub fn twice_partial<T, F>(f: F) -> impl Fn(T) -> T
where
F: Fn(T) -> T,
{
// `f` is moved into the closure. Because F: Fn (not FnOnce), calling
// the closure multiple times is safe — f itself is never consumed.
move |x| f(f(x))
}
// ── Implementation 3: Function-pointer variant ───────────────────────────────
//
// `fn(i32) -> i32` is a bare function pointer, not a closure.
// This is less general (no captured environment) but zero-overhead and
// useful when you only need named free functions like `double` / `square`.
/// Apply a bare function pointer twice (no closure capture).
pub fn twice_fp(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// ── Named helpers matching the OCaml originals ───────────────────────────────
pub fn double(x: i32) -> i32 {
2 * x
}
pub fn square(x: i32) -> i32 {
x * x
}
// ── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// --- twice (generic) ---
#[test]
fn test_twice_double() {
// double(double(3)) = double(6) = 12
assert_eq!(twice(double, 3), 12);
}
#[test]
fn test_twice_square() {
// square(square(2)) = square(4) = 16
assert_eq!(twice(square, 2), 16);
}
#[test]
fn test_twice_with_closure() {
// Works with any Fn(T) -> T, including closures
let increment = |x: i32| x + 1;
assert_eq!(twice(increment, 5), 7);
}
#[test]
fn test_twice_identity() {
assert_eq!(twice(|x: i32| x, 42), 42);
}
// --- twice_partial (curried / partial application) ---
#[test]
fn test_partial_quad() {
// quad = twice double — bind double, get a new function
let quad = twice_partial(double);
assert_eq!(quad(3), 12);
assert_eq!(quad(0), 0);
assert_eq!(quad(-1), -4);
}
#[test]
fn test_partial_fourth() {
// fourth = twice square — x^4
let fourth = twice_partial(square);
assert_eq!(fourth(2), 16);
assert_eq!(fourth(3), 81);
}
#[test]
fn test_partial_reusable() {
// The returned closure can be called multiple times
let quad = twice_partial(|x: i32| 2 * x);
let results: Vec<i32> = (1..=4).map(|x| quad(x)).collect();
assert_eq!(results, vec![4, 8, 12, 16]);
}
// --- twice_fp (function pointer) ---
#[test]
fn test_fp_double() {
assert_eq!(twice_fp(double, 3), 12);
}
#[test]
fn test_fp_square() {
assert_eq!(twice_fp(square, 2), 16);
}
}#[cfg(test)]
mod tests {
use super::*;
// --- twice (generic) ---
#[test]
fn test_twice_double() {
// double(double(3)) = double(6) = 12
assert_eq!(twice(double, 3), 12);
}
#[test]
fn test_twice_square() {
// square(square(2)) = square(4) = 16
assert_eq!(twice(square, 2), 16);
}
#[test]
fn test_twice_with_closure() {
// Works with any Fn(T) -> T, including closures
let increment = |x: i32| x + 1;
assert_eq!(twice(increment, 5), 7);
}
#[test]
fn test_twice_identity() {
assert_eq!(twice(|x: i32| x, 42), 42);
}
// --- twice_partial (curried / partial application) ---
#[test]
fn test_partial_quad() {
// quad = twice double — bind double, get a new function
let quad = twice_partial(double);
assert_eq!(quad(3), 12);
assert_eq!(quad(0), 0);
assert_eq!(quad(-1), -4);
}
#[test]
fn test_partial_fourth() {
// fourth = twice square — x^4
let fourth = twice_partial(square);
assert_eq!(fourth(2), 16);
assert_eq!(fourth(3), 81);
}
#[test]
fn test_partial_reusable() {
// The returned closure can be called multiple times
let quad = twice_partial(|x: i32| 2 * x);
let results: Vec<i32> = (1..=4).map(|x| quad(x)).collect();
assert_eq!(results, vec![4, 8, 12, 16]);
}
// --- twice_fp (function pointer) ---
#[test]
fn test_fp_double() {
assert_eq!(twice_fp(double, 3), 12);
}
#[test]
fn test_fp_square() {
assert_eq!(twice_fp(square, 2), 16);
}
}
Deep Comparison
Side-by-Side: Applying a Function Twice
OCaml
let twice f x = f (f x)
let double x = 2 * x
let square x = x * x
let quad = twice double (* partial application — no argument given *)
let fourth = twice square
let () =
Printf.printf "quad 3 = %d\n" (quad 3); (* 12 *)
Printf.printf "fourth 2 = %d\n" (fourth 2) (* 16 *)
Rust — Implementation 1: Generic (direct form)
pub fn twice<T, F>(f: F, x: T) -> T
where
F: Fn(T) -> T,
{
f(f(x))
}
// Usage (both arguments supplied):
twice(double, 3) // 12
twice(square, 2) // 16
Rust — Implementation 2: Partial application (curried style)
pub fn twice_partial<T, F>(f: F) -> impl Fn(T) -> T
where
F: Fn(T) -> T,
{
move |x| f(f(x)) // f captured by move into the returned closure
}
// Usage — mirrors OCaml exactly:
let quad = twice_partial(double); // quad: impl Fn(i32) -> i32
let fourth = twice_partial(square);
quad(3) // 12
fourth(2) // 16
Rust — Implementation 3: Function pointer variant
pub fn twice_fp(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// fn(i32) -> i32 — bare function pointer, no captured environment.
twice_fp(double, 3) // 12
twice_fp(square, 2) // 16
Concept Map
| OCaml concept | Rust equivalent |
|---|---|
Curried function ('a -> 'a) -> 'a -> 'a | Two-argument generic fn or impl Fn return |
let quad = twice double | let quad = twice_partial(double) |
Polymorphic 'a | Generic type parameter T |
| All functions implicitly curried | Closures capture by move; explicit impl Fn(T) -> T |
| Function value (no distinction) | Fn trait (closure) vs fn pointer |
Why Fn and not FnOnce?
FnOnce means the closure can only be called once (it consumes captured values).
Here f is called twice inside the body, so it must be Fn (callable by shared
reference, any number of times). FnMut would also work but is unnecessarily
broad when no mutation is needed.
Ownership note on twice_partial
// f is moved into the returned closure.
// The closure itself is Fn because F: Fn — it can be called many times.
// Each call to the returned closure borrows f immutably to invoke f(f(x)).
move |x| f(f(x))
The intermediate value f(x): T is owned by the stack frame and then moved
into the second call f(…). No heap allocation is required.
Exercises
apply_n<T: Clone, F: Fn(T) -> T>(f: F, n: usize, x: T) -> T that applies f exactly n times. Handle n=0 (return x unchanged).fixed_point<T: Clone + PartialEq, F: Fn(T) -> T>(f: F, x: T) -> T that applies f repeatedly until f(x) == x (convergence). Use this to compute square roots by Newton's method.compose(f, g) in terms of twice — or explain why it cannot be expressed that way without additional combinators.apply_n<T>(f: impl Fn(T) -> T, n: usize, x: T) -> T that applies f exactly n times. For n=0, return x. For n=1, return f(x). Connect this to Church numeral representation.fixed_point<T: PartialEq + Clone>(f: impl Fn(T) -> T, x: T, max_iter: usize) -> Option<T> that keeps applying f until the output equals the input (or exceeds max_iter). Useful for iterative numerical methods.