Closure as Argument
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
F: Fn(A) -> B bound parametersapply, apply_twice, and compose as higher-order functionsfilter_with, map_with, and reduce_with wrappers around iterator methodsimpl Fn in argument position monomorphises (static dispatch, zero overhead)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
F: Fn(A) -> B bounds; OCaml infers the function type automatically.impl Fn argument generates a separate specialised function per call site; OCaml's functions are polymorphic at runtime via uniform representation.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);
}
}#[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.and_pred, or_pred, and not_pred that take Fn(&T) -> bool closures and return new closures combining them with &&, ||, and !.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]).