๐Ÿฆ€ Functional Rust
๐ŸŽฌ Closures in Rust Fn/FnMut/FnOnce, capturing environment, move closures, higher-order functions.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Closures capture variables from their environment โ€” by reference, mutable reference, or by value (move)

โ€ข Three traits: Fn (shared borrow), FnMut (mutable borrow), FnOnce (takes ownership)

โ€ข Higher-order functions like .map(), .filter(), .fold() accept closures as arguments

โ€ข move closures take ownership of captured variables โ€” essential for threading

โ€ข Closures enable functional patterns: partial application, composition, and strategy

122: Higher-Order Functions with Lifetime Constraints

Difficulty: 3 Level: Intermediate Write functions that accept and return other functions โ€” and annotate lifetimes when those functions deal with references.

The Problem This Solves

Higher-order functions are natural in Rust โ€” pass a closure, return a closure, compose two functions โ€” until references are involved. When a function takes a reference as input and returns a reference derived from it, the compiler needs to know: how long does the output reference live? If it comes from the input, the output can't outlive the input. Without lifetime annotations, the compiler can't verify this and rejects the code. This isn't just bureaucracy. It prevents a real class of bugs. In C, returning a pointer to a local variable compiles fine and produces undefined behavior at runtime. In Rust, forgetting to connect input and output lifetimes is a compile error. The annotation is how you tell the compiler "these references have the same lifetime" โ€” and it checks that claim throughout the call site. The practical relief: when you return owned data (`String`, `Vec<T>`) from a higher-order function, no lifetime annotations are needed. Ownership and borrowing are separate concerns โ€” only borrows need lifetime tracking.

The Intuition

Lifetime annotations on higher-order functions are promises to the compiler: "this output reference comes from this input reference, so they live the same amount of time."

How It Works in Rust

// HOF returning a reference โ€” lifetime 'a connects input to output
fn find_first<'a, F>(items: &'a [&'a str], pred: F) -> Option<&'a str>
where
 F: Fn(&str) -> bool,
{
 // The returned &str points into `items`, which lives for 'a.
 // Without 'a, the compiler can't know that the output is valid.
 items.iter().copied().find(|&s| pred(s))
}

let data = vec!["apple", "banana", "cherry"];
let long = find_first(&data, |s| s.len() > 5);
assert_eq!(long, Some("banana"));

// Function composition โ€” no lifetimes needed (owned values)
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))  // f and g are moved into the returned closure
}

let double_then_add = compose(|x: i32| x + 1, |x: i32| x * 2);
assert_eq!(double_then_add(5), 11);  // (5*2)+1 = 11

// HOF that returns owned data โ€” no lifetime annotation needed
fn transform_all(items: &[&str], f: impl Fn(&str) -> String) -> Vec<String> {
 items.iter().map(|&s| f(s)).collect()
}
let lower = transform_all(&["Hello", "WORLD"], |s| s.to_lowercase());

What This Unlocks

Key Differences

ConceptOCamlRust
HOF with referencesNo annotations โ€” GC manages lifetimesLifetime parameters required
Function compositionSimple: `fun x -> f (g x)`Generic bounds: `F: Fn(B) -> C, G: Fn(A) -> B`
Return references from inputsGC keeps input alive automatically`'a` lifetime links input to output
Returning owned dataN/ANo lifetimes needed โ€” ownership is sufficient
// Example 122: Higher-Order Functions with Lifetime Constraints
//
// When HOFs deal with references, lifetimes must be explicit
// to tell the compiler how long returned references live.

// Approach 1: HOF returning a reference โ€” needs lifetime
fn find_first<'a, F>(items: &'a [&'a str], pred: F) -> Option<&'a str>
where
    F: Fn(&str) -> bool,
{
    items.iter().copied().find(|&s| pred(s))
}

fn approach1() {
    let data = vec!["apple", "banana", "cherry", "date"];
    let long = find_first(&data, |s| s.len() > 5);
    assert_eq!(long, Some("banana"));
    println!("First long: {}", long.unwrap());
}

// Approach 2: Function composition
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))
}

fn pipe<A, B, F: Fn(A) -> B>(x: A, f: F) -> B {
    f(x)
}

fn approach2() {
    let double = |x: i32| x * 2;
    let add1 = |x: i32| x + 1;
    let double_then_add = compose(add1, double);
    assert_eq!(double_then_add(5), 11);
    
    let result = pipe(pipe(5, double), add1);
    assert_eq!(result, 11);
    println!("compose(add1, double)(5) = {}", double_then_add(5));
}

// Approach 3: HOF on borrowed slices with lifetime propagation
fn transform_all<'a, F>(items: &[&'a str], f: F) -> Vec<String>
where
    F: Fn(&str) -> String,
{
    items.iter().map(|&s| f(s)).collect()
}

// HOF that returns references from the input
fn filter_refs<'a, F>(items: &'a [String], pred: F) -> Vec<&'a str>
where
    F: Fn(&str) -> bool,
{
    items.iter()
        .filter(|s| pred(s))
        .map(|s| s.as_str())
        .collect()
}

fn approach3() {
    let words = ["hello", "WORLD", "Rust"];
    let lower = transform_all(&words, |s| s.to_lowercase());
    assert_eq!(lower, vec!["hello", "world", "rust"]);
    
    let owned = vec!["hello".into(), "WORLD".into(), "Rust".into()];
    let filtered = filter_refs(&owned, |s| s.chars().next().map_or(false, |c| c.is_uppercase()));
    assert_eq!(filtered, vec!["WORLD", "Rust"]);
    
    println!("Lower: {:?}", lower);
    println!("Uppercase starts: {:?}", filtered);
}

fn main() {
    println!("=== Approach 1: Find with Predicate ===");
    approach1();
    println!("\n=== Approach 2: Composition ===");
    approach2();
    println!("\n=== Approach 3: Transform Borrowed Data ===");
    approach3();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_first() {
        let data = vec!["a", "bb", "ccc"];
        assert_eq!(find_first(&data, |s| s.len() >= 2), Some("bb"));
        assert_eq!(find_first(&data, |s| s.len() >= 5), None);
    }

    #[test]
    fn test_compose() {
        let f = compose(|x: i32| x.to_string(), |x: i32| x * 2);
        assert_eq!(f(5), "10");
    }

    #[test]
    fn test_pipe() {
        assert_eq!(pipe(5, |x| x * 3), 15);
    }

    #[test]
    fn test_transform_all() {
        let items = ["abc", "DEF"];
        let result = transform_all(&items, |s| s.to_uppercase());
        assert_eq!(result, vec!["ABC", "DEF"]);
    }

    #[test]
    fn test_filter_refs_lifetime() {
        let owned = vec!["hello".to_string(), "world".to_string()];
        let refs = filter_refs(&owned, |s| s.starts_with('h'));
        assert_eq!(refs, vec!["hello"]);
    }
}
(* Example 122: Higher-Order Functions with Lifetime Constraints *)

(* OCaml HOFs work without worrying about lifetimes.
   Rust HOFs that take/return references need lifetime annotations. *)

(* Approach 1: Function that takes a predicate and returns matching slice *)
let find_first pred lst =
  try Some (List.find pred lst)
  with Not_found -> None

let approach1 () =
  let data = ["apple"; "banana"; "cherry"; "date"] in
  let long = find_first (fun s -> String.length s > 5) data in
  assert (long = Some "banana");
  Printf.printf "First long: %s\n" (Option.get long)

(* Approach 2: Composing functions *)
let compose f g x = f (g x)
let pipe x f = f x

let approach2 () =
  let double = ( * ) 2 in
  let add1 = ( + ) 1 in
  let double_then_add = compose add1 double in
  assert (double_then_add 5 = 11);
  let result = pipe 5 double |> add1 in
  assert (result = 11);
  Printf.printf "compose(add1, double)(5) = %d\n" (double_then_add 5)

(* Approach 3: Applying transformations to borrowed data *)
let transform_all f items =
  List.map f items

let approach3 () =
  let words = ["hello"; "WORLD"; "Rust"] in
  let lower = transform_all String.lowercase_ascii words in
  assert (lower = ["hello"; "world"; "rust"]);
  Printf.printf "Lowered: %s\n" (String.concat ", " lower)

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Higher-Order Functions

Finding with Predicate

OCaml:

๐Ÿช Show OCaml equivalent
let find_first pred lst =
try Some (List.find pred lst)
with Not_found -> None

Rust โ€” lifetime connects input to output:

fn find_first<'a, F>(items: &'a [&'a str], pred: F) -> Option<&'a str>
where F: Fn(&str) -> bool {
 items.iter().copied().find(|&s| pred(s))
}

Function Composition

OCaml:

๐Ÿช Show OCaml equivalent
let compose f g x = f (g x)

Rust:

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

Transforming Borrowed Data

OCaml:

๐Ÿช Show OCaml equivalent
let transform_all f items = List.map f items

Rust:

fn transform_all<'a, F>(items: &[&'a str], f: F) -> Vec<String>
where F: Fn(&str) -> String {
 items.iter().map(|&s| f(s)).collect()
}