Closure as Return
Tutorial Video
Text description (accessibility)
This video demonstrates the "Closure as Return" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. A function that returns a specialised function is a closure factory. Key difference from OCaml: 1. **`impl Fn` vs. `Box<dyn Fn>`**: Rust's `impl Fn` is zero
Tutorial
The Problem
A function that returns a specialised function is a closure factory. make_adder(5) returns a function that adds 5 to its argument — a specific case of partial application. make_counter() returns a stateful function that increments a counter on each call. These patterns are everywhere: middleware builders, parser combinators, event handlers, configuration-driven pipelines. Rust requires the return type to be concrete: impl Fn(i32) -> i32 for static dispatch (inlined by the compiler) or Box<dyn Fn(i32) -> i32> when type erasure is needed for heterogeneous collections.
🎯 Learning Outcomes
impl Fn(A) -> B for zero-cost static dispatchimpl FnMut() -> T for generator patternsBox<dyn Fn> when the concrete type must be hidden or stored heterogeneouslymoveimpl Trait in return position infers one concrete type per call siteCode Example
#![allow(clippy::all)]
//! # Closure as Return — Returning Closures
/// Return closure with impl Trait
pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
/// Return closure that captures multiple values
pub fn make_linear(slope: f64, intercept: f64) -> impl Fn(f64) -> f64 {
move |x| slope * x + intercept
}
/// Return closure that maintains state (using RefCell)
pub fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
/// Return closure factory
pub fn make_multiplier_factory() -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> {
|factor| Box::new(move |x| x * factor)
}
/// Generic closure return
pub fn make_mapper<T, F: Fn(T) -> T + 'static>(f: F) -> Box<dyn Fn(T) -> T>
where
T: 'static,
{
Box::new(f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
}
#[test]
fn test_make_linear() {
let f = make_linear(2.0, 1.0); // y = 2x + 1
assert_eq!(f(3.0), 7.0);
}
#[test]
fn test_counter() {
let mut counter = make_counter();
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
assert_eq!(counter(), 3);
}
#[test]
fn test_factory() {
let factory = make_multiplier_factory();
let times3 = factory(3);
let times5 = factory(5);
assert_eq!(times3(10), 30);
assert_eq!(times5(10), 50);
}
#[test]
fn test_generic_mapper() {
let double = make_mapper(|x: i32| x * 2);
assert_eq!(double(21), 42);
}
}Key Differences
impl Fn vs. Box<dyn Fn>**: Rust's impl Fn is zero-cost (monomorphised); Box<dyn Fn> adds a heap allocation and vtable dispatch. OCaml has uniform representation — no such distinction.FnMut return**: Rust's make_counter returns impl FnMut() — callers must declare mut counter. OCaml's counter closure mutates via ref without any declaration.impl Fn return's lifetime is tied to the closure's captures (the compiler infers this); OCaml's GC manages all lifetimes.impl Fn; OCaml infers the function type directly.OCaml Approach
OCaml functions naturally return closures — no special syntax required:
let make_adder n = fun x -> x + n
let make_linear slope intercept = fun x -> slope *. x +. intercept
let make_counter () =
let count = ref 0 in
fun () -> incr count; !count
OCaml's ref provides the mutable state for stateful closures; there is no mut annotation on the returned function.
Full Source
#![allow(clippy::all)]
//! # Closure as Return — Returning Closures
/// Return closure with impl Trait
pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
/// Return closure that captures multiple values
pub fn make_linear(slope: f64, intercept: f64) -> impl Fn(f64) -> f64 {
move |x| slope * x + intercept
}
/// Return closure that maintains state (using RefCell)
pub fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
/// Return closure factory
pub fn make_multiplier_factory() -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> {
|factor| Box::new(move |x| x * factor)
}
/// Generic closure return
pub fn make_mapper<T, F: Fn(T) -> T + 'static>(f: F) -> Box<dyn Fn(T) -> T>
where
T: 'static,
{
Box::new(f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
}
#[test]
fn test_make_linear() {
let f = make_linear(2.0, 1.0); // y = 2x + 1
assert_eq!(f(3.0), 7.0);
}
#[test]
fn test_counter() {
let mut counter = make_counter();
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
assert_eq!(counter(), 3);
}
#[test]
fn test_factory() {
let factory = make_multiplier_factory();
let times3 = factory(3);
let times5 = factory(5);
assert_eq!(times3(10), 30);
assert_eq!(times5(10), 50);
}
#[test]
fn test_generic_mapper() {
let double = make_mapper(|x: i32| x * 2);
assert_eq!(double(21), 42);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
assert_eq!(add5(0), 5);
}
#[test]
fn test_make_linear() {
let f = make_linear(2.0, 1.0); // y = 2x + 1
assert_eq!(f(3.0), 7.0);
}
#[test]
fn test_counter() {
let mut counter = make_counter();
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
assert_eq!(counter(), 3);
}
#[test]
fn test_factory() {
let factory = make_multiplier_factory();
let times3 = factory(3);
let times5 = factory(5);
assert_eq!(times3(10), 30);
assert_eq!(times5(10), 50);
}
#[test]
fn test_generic_mapper() {
let double = make_mapper(|x: i32| x * 2);
assert_eq!(double(21), 42);
}
}
Deep Comparison
Closure As Return: Comparison
See src/lib.rs for the Rust implementation.
Exercises
fn make_fib() -> impl FnMut() -> u64 that returns successive Fibonacci numbers on each call, maintaining (a, b) state.fn make_rate_limiter(max_per_sec: u32) -> impl FnMut() -> bool using std::time::Instant that returns true when the call is within the rate limit and false when exceeded.fn make_pipeline<A, B, C>(f: impl Fn(A)->B, g: impl Fn(B)->C) -> impl Fn(A)->C that chains two transformations and verify it composes with a third.