ExamplesBy LevelBy TopicLearning Paths
503 Intermediate

Closure as Argument

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure as Argument" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. A function that hardcodes its operation is inflexible. Key difference from OCaml: 1. **Trait bounds**: Rust requires explicit `F: Fn(A)

Tutorial

The Problem

A function that hardcodes its operation is inflexible. Vec::sort() only sorts in ascending order; sort_by(|a, b| b.cmp(a)) sorts in descending order — same algorithm, different comparator. The ability to pass behaviour as an argument (higher-order functions) is the core of functional programming: it separates the structure of a computation (iterate, fold, filter) from the policy (what to do at each step). Rust's impl Fn bound enables this with zero runtime overhead through monomorphisation.

🎯 Learning Outcomes

  • • Accept a closure with F: Fn(A) -> B bound parameters
  • • Implement apply, apply_twice, and compose as higher-order functions
  • • Build filter_with, map_with, and reduce_with wrappers around iterator methods
  • • Understand that impl Fn in argument position monomorphises (static dispatch, zero overhead)
  • • Compose two closures into a new closure using captured ownership
  • Code Example

    #![allow(clippy::all)]
    //! # Closure as Argument — Higher-Order Functions
    
    pub fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
        f(x)
    }
    
    pub fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
        f(f(x))
    }
    
    pub fn compose<F, G>(f: F, g: G) -> impl Fn(i32) -> i32
    where
        F: Fn(i32) -> i32,
        G: Fn(i32) -> i32,
    {
        move |x| f(g(x))
    }
    
    pub fn filter_with<F: Fn(&i32) -> bool>(items: Vec<i32>, predicate: F) -> Vec<i32> {
        items.into_iter().filter(predicate).collect()
    }
    
    pub fn map_with<F: Fn(i32) -> i32>(items: Vec<i32>, mapper: F) -> Vec<i32> {
        items.into_iter().map(mapper).collect()
    }
    
    pub fn reduce_with<F: Fn(i32, i32) -> i32>(items: Vec<i32>, initial: i32, reducer: F) -> i32 {
        items.into_iter().fold(initial, reducer)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply() {
            assert_eq!(apply(|x| x + 1, 41), 42);
        }
    
        #[test]
        fn test_apply_twice() {
            assert_eq!(apply_twice(|x| x * 2, 5), 20);
        }
    
        #[test]
        fn test_compose() {
            let f = compose(|x| x + 1, |x| x * 2);
            assert_eq!(f(5), 11); // (5*2)+1
        }
    
        #[test]
        fn test_filter() {
            let v = filter_with(vec![1, 2, 3, 4, 5], |&x| x % 2 == 0);
            assert_eq!(v, vec![2, 4]);
        }
    
        #[test]
        fn test_map() {
            let v = map_with(vec![1, 2, 3], |x| x * x);
            assert_eq!(v, vec![1, 4, 9]);
        }
    
        #[test]
        fn test_reduce() {
            let sum = reduce_with(vec![1, 2, 3, 4], 0, |a, b| a + b);
            assert_eq!(sum, 10);
        }
    }

    Key Differences

  • Trait bounds: Rust requires explicit F: Fn(A) -> B bounds; OCaml infers the function type automatically.
  • Monomorphisation: Rust's impl Fn argument generates a separate specialised function per call site; OCaml's functions are polymorphic at runtime via uniform representation.
  • Composition ownership: Rust's compose requires move to capture f and g by value; OCaml captures by reference automatically.
  • **filter_with vs. List.filter**: Rust builds these as wrappers around Iterator methods; OCaml's List.filter is already the higher-order version.
  • OCaml Approach

    OCaml's functions are first-class without any trait declaration:

    let apply f x = f x
    let apply_twice f x = f (f x)
    let compose f g x = f (g x)
    
    let filter_with pred items = List.filter pred items
    let map_with f items = List.map f items
    let reduce_with f init items = List.fold_left f init items
    

    OCaml's |> pipe operator and @@ application operator complement function composition:

    5 |> (fun x -> x * 2) |> (fun x -> x + 1)  (* 11 *)
    

    Full Source

    #![allow(clippy::all)]
    //! # Closure as Argument — Higher-Order Functions
    
    pub fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
        f(x)
    }
    
    pub fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
        f(f(x))
    }
    
    pub fn compose<F, G>(f: F, g: G) -> impl Fn(i32) -> i32
    where
        F: Fn(i32) -> i32,
        G: Fn(i32) -> i32,
    {
        move |x| f(g(x))
    }
    
    pub fn filter_with<F: Fn(&i32) -> bool>(items: Vec<i32>, predicate: F) -> Vec<i32> {
        items.into_iter().filter(predicate).collect()
    }
    
    pub fn map_with<F: Fn(i32) -> i32>(items: Vec<i32>, mapper: F) -> Vec<i32> {
        items.into_iter().map(mapper).collect()
    }
    
    pub fn reduce_with<F: Fn(i32, i32) -> i32>(items: Vec<i32>, initial: i32, reducer: F) -> i32 {
        items.into_iter().fold(initial, reducer)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply() {
            assert_eq!(apply(|x| x + 1, 41), 42);
        }
    
        #[test]
        fn test_apply_twice() {
            assert_eq!(apply_twice(|x| x * 2, 5), 20);
        }
    
        #[test]
        fn test_compose() {
            let f = compose(|x| x + 1, |x| x * 2);
            assert_eq!(f(5), 11); // (5*2)+1
        }
    
        #[test]
        fn test_filter() {
            let v = filter_with(vec![1, 2, 3, 4, 5], |&x| x % 2 == 0);
            assert_eq!(v, vec![2, 4]);
        }
    
        #[test]
        fn test_map() {
            let v = map_with(vec![1, 2, 3], |x| x * x);
            assert_eq!(v, vec![1, 4, 9]);
        }
    
        #[test]
        fn test_reduce() {
            let sum = reduce_with(vec![1, 2, 3, 4], 0, |a, b| a + b);
            assert_eq!(sum, 10);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply() {
            assert_eq!(apply(|x| x + 1, 41), 42);
        }
    
        #[test]
        fn test_apply_twice() {
            assert_eq!(apply_twice(|x| x * 2, 5), 20);
        }
    
        #[test]
        fn test_compose() {
            let f = compose(|x| x + 1, |x| x * 2);
            assert_eq!(f(5), 11); // (5*2)+1
        }
    
        #[test]
        fn test_filter() {
            let v = filter_with(vec![1, 2, 3, 4, 5], |&x| x % 2 == 0);
            assert_eq!(v, vec![2, 4]);
        }
    
        #[test]
        fn test_map() {
            let v = map_with(vec![1, 2, 3], |x| x * x);
            assert_eq!(v, vec![1, 4, 9]);
        }
    
        #[test]
        fn test_reduce() {
            let sum = reduce_with(vec![1, 2, 3, 4], 0, |a, b| a + b);
            assert_eq!(sum, 10);
        }
    }

    Deep Comparison

    Closure As Argument: Comparison

    See src/lib.rs for the Rust implementation.

    Exercises

  • **apply_n**: Write fn apply_n<F: Fn(i32) -> i32>(f: F, x: i32, n: usize) -> i32 that applies f exactly n times.
  • Predicate combinators: Write and_pred, or_pred, and not_pred that take Fn(&T) -> bool closures and return new closures combining them with &&, ||, and !.
  • Iterator adaptor: Write fn window_map<F: Fn(i32, i32) -> i32>(data: &[i32], f: F) -> Vec<i32> that applies f to each consecutive pair (data[i], data[i+1]).
  • Open Source Repos