Higher-Order Functions
Tutorial Video
Text description (accessibility)
This video demonstrates the "Higher-Order Functions" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Higher-order functions (HOFs) — functions that take or return other functions — are the backbone of functional programming. Key difference from OCaml: 1. **Iterator vs recursion**: Rust HOFs operate on `Iterator` chains with lazy evaluation and optional collect; OCaml HOFs typically process `list` values recursively, though `Seq` provides laziness.
Tutorial
The Problem
Higher-order functions (HOFs) — functions that take or return other functions — are the backbone of functional programming. They emerged from lambda calculus (Alonzo Church, 1930s) and appeared in practical form in LISP, ML, and Haskell. Rust's iterator API is built entirely on HOFs: map, filter, fold, flat_map, zip. Beyond the standard library, custom HOFs like zip_with, scan_left, and group_by allow expressing complex data transformations as pipelines without intermediate allocations or imperative loops.
🎯 Learning Outcomes
Fn boundsmap/filter (element-wise) and fold/scan (accumulating)zip_with generalizes zip + map into a single combining stepscan_left produces all intermediate fold values (useful for running totals)partition_by and group_by using closures as classifiersCode Example
pub fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
where F: Fn(&A, &B) -> C {
a.iter().zip(b.iter()).map(|(x, y)| f(x, y)).collect()
}
vec![1, 2, 3].iter().map(|&x| x * 2).collect::<Vec<_>>()Key Differences
Iterator chains with lazy evaluation and optional collect; OCaml HOFs typically process list values recursively, though Seq provides laziness.&[T] slices avoid allocation in HOFs that don't need ownership; OCaml lists are always heap-allocated linked lists.Fn, FnMut, or FnOnce bounds on HOF parameters; OCaml infers the function type structurally without named bounds.impl Fn cannot name the concrete type; OCaml HOFs return plain function types that are fully transparent to the type checker.OCaml Approach
OCaml's List module provides map, filter, fold_left, fold_right, and partition as stdlib HOFs. Custom combinators like zip_with and scan_left are straightforward to write and compose. OCaml's structural tail-call optimization makes recursive HOFs efficient without iterators.
let zip_with f xs ys = List.map2 f xs ys
let scan_left f init xs =
List.fold_left (fun (acc, res) x ->
let acc' = f acc x in (acc', acc' :: res)
) (init, [init]) xs |> snd |> List.rev
Full Source
#![allow(clippy::all)]
//! Higher-Order Functions
//!
//! Rust's iterator HOFs: map, filter, fold, flat_map, zip, and custom ones.
/// Custom HOF: zip two slices with a combining function.
pub fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
where
F: Fn(&A, &B) -> C,
{
a.iter().zip(b.iter()).map(|(x, y)| f(x, y)).collect()
}
/// Custom HOF: scan (like fold but returns all intermediate values).
pub fn scan_left<T: Clone, U: Clone, F>(items: &[T], init: U, f: F) -> Vec<U>
where
F: Fn(U, &T) -> U,
{
let mut acc = init;
let mut result = vec![acc.clone()];
for item in items {
acc = f(acc, item);
result.push(acc.clone());
}
result
}
/// Custom HOF: partition by predicate.
pub fn partition_by<T, P>(items: Vec<T>, pred: P) -> (Vec<T>, Vec<T>)
where
P: Fn(&T) -> bool,
{
items.into_iter().partition(pred)
}
/// Custom HOF: take while predicate holds.
pub fn take_while_custom<T, P>(items: &[T], pred: P) -> Vec<T>
where
T: Clone,
P: Fn(&T) -> bool,
{
items.iter().take_while(|x| pred(x)).cloned().collect()
}
/// Custom HOF: group consecutive elements.
pub fn group_by<T: Clone + PartialEq>(items: &[T]) -> Vec<Vec<T>> {
if items.is_empty() {
return vec![];
}
let mut groups = vec![vec![items[0].clone()]];
for item in items.iter().skip(1) {
if groups.last().unwrap().last() == Some(item) {
groups.last_mut().unwrap().push(item.clone());
} else {
groups.push(vec![item.clone()]);
}
}
groups
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zip_with_add() {
let a = [1, 2, 3];
let b = [10, 20, 30];
let result = zip_with(&a, &b, |x, y| x + y);
assert_eq!(result, vec![11, 22, 33]);
}
#[test]
fn test_zip_with_concat() {
let a = ["a", "b"];
let b = ["1", "2"];
let result: Vec<String> = zip_with(&a, &b, |x, y| format!("{}{}", x, y));
assert_eq!(result, vec!["a1", "b2"]);
}
#[test]
fn test_scan_left_sum() {
let nums = [1, 2, 3, 4];
let result = scan_left(&nums, 0, |acc, &x| acc + x);
assert_eq!(result, vec![0, 1, 3, 6, 10]);
}
#[test]
fn test_partition_by() {
let (evens, odds) = partition_by(vec![1, 2, 3, 4, 5, 6], |x| x % 2 == 0);
assert_eq!(evens, vec![2, 4, 6]);
assert_eq!(odds, vec![1, 3, 5]);
}
#[test]
fn test_take_while_custom() {
let nums = [1, 2, 3, 10, 4, 5];
let result = take_while_custom(&nums, |&x| x < 5);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_group_by() {
let items = [1, 1, 2, 2, 2, 3, 1, 1];
let groups = group_by(&items);
assert_eq!(groups, vec![vec![1, 1], vec![2, 2, 2], vec![3], vec![1, 1]]);
}
#[test]
fn test_standard_hofs() {
let nums = vec![1, 2, 3, 4, 5];
// map
let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);
// filter
let evens: Vec<i32> = nums.iter().filter(|&&x| x % 2 == 0).cloned().collect();
assert_eq!(evens, vec![2, 4]);
// fold
let sum: i32 = nums.iter().fold(0, |acc, &x| acc + x);
assert_eq!(sum, 15);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zip_with_add() {
let a = [1, 2, 3];
let b = [10, 20, 30];
let result = zip_with(&a, &b, |x, y| x + y);
assert_eq!(result, vec![11, 22, 33]);
}
#[test]
fn test_zip_with_concat() {
let a = ["a", "b"];
let b = ["1", "2"];
let result: Vec<String> = zip_with(&a, &b, |x, y| format!("{}{}", x, y));
assert_eq!(result, vec!["a1", "b2"]);
}
#[test]
fn test_scan_left_sum() {
let nums = [1, 2, 3, 4];
let result = scan_left(&nums, 0, |acc, &x| acc + x);
assert_eq!(result, vec![0, 1, 3, 6, 10]);
}
#[test]
fn test_partition_by() {
let (evens, odds) = partition_by(vec![1, 2, 3, 4, 5, 6], |x| x % 2 == 0);
assert_eq!(evens, vec![2, 4, 6]);
assert_eq!(odds, vec![1, 3, 5]);
}
#[test]
fn test_take_while_custom() {
let nums = [1, 2, 3, 10, 4, 5];
let result = take_while_custom(&nums, |&x| x < 5);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_group_by() {
let items = [1, 1, 2, 2, 2, 3, 1, 1];
let groups = group_by(&items);
assert_eq!(groups, vec![vec![1, 1], vec![2, 2, 2], vec![3], vec![1, 1]]);
}
#[test]
fn test_standard_hofs() {
let nums = vec![1, 2, 3, 4, 5];
// map
let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);
// filter
let evens: Vec<i32> = nums.iter().filter(|&&x| x % 2 == 0).cloned().collect();
assert_eq!(evens, vec![2, 4]);
// fold
let sum: i32 = nums.iter().fold(0, |acc, &x| acc + x);
assert_eq!(sum, 15);
}
}
Deep Comparison
OCaml vs Rust: Higher-Order Functions
OCaml
let zip_with f a b = List.map2 f a b
let scan_left f init = List.fold_left (fun (acc, xs) x ->
let next = f acc x in (next, xs @ [next])) (init, [init])
List.map (fun x -> x * 2) [1; 2; 3]
List.filter (fun x -> x mod 2 = 0) [1; 2; 3; 4]
List.fold_left (+) 0 [1; 2; 3; 4; 5]
Rust
pub fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
where F: Fn(&A, &B) -> C {
a.iter().zip(b.iter()).map(|(x, y)| f(x, y)).collect()
}
vec![1, 2, 3].iter().map(|&x| x * 2).collect::<Vec<_>>()
Key Differences
Exercises
unfold**: Implement unfold<T, S, F>(seed: S, f: F) -> Vec<T> where F: Fn(S) -> Option<(T, S)> that generates a vector by repeatedly applying f until it returns None.chunk_by**: Write a HOF that groups elements into fixed-size chunks, returning a Vec<Vec<T>>, handling the remainder chunk if the length is not divisible.find_map**: Implement find_map<T, U, F>(items: &[T], f: F) -> Option<U> where F: Fn(&T) -> Option<U> that returns the first Some result from applying f to each element.