Closures & Iterators
TypeScript arrow functions vs Rust closures
Section titled “TypeScript arrow functions vs Rust closures”In TypeScript, arrow functions can capture variables from their enclosing scope freely. Rust closures do the same, but the compiler tracks exactly how the closure uses its captured values and enforces ownership rules at compile time.
// TypeScript — arrow function captures 'factor' by referenceconst factor = 3;const triple = (x: number) => x * factor;console.log(triple(5)); // 15
// Closures in higher-order functionsconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(x => x * 2);console.log(doubled); // [2, 4, 6, 8, 10]
// Capturing mutable state — counterlet count = 0;const increment = () => { count++; };increment();increment();console.log(count); // 2fn main() { // Closure captures 'factor' by reference (immutable) let factor = 3; let triple = |x| x * factor; println!("{}", triple(5)); // 15
// Iterator adapters: map, filter, collect let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect(); println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// FnMut closure — captures count mutably let mut count = 0; let mut increment = || { count += 1; }; increment(); increment(); drop(increment); // release borrow so we can use count again println!("{count}"); // 2}The Fn trait hierarchy
Section titled “The Fn trait hierarchy”Rust models closure capabilities as three traits. Understanding them is important when you pass closures to functions or store them in structs.
| Trait | What it can do | TypeScript analogy |
|---|---|---|
FnOnce | Called at most once; can move captured values out | A one-shot callback that consumes its captures |
FnMut | Called multiple times; mutates captures | A stateful callback that modifies closed-over variables |
Fn | Called multiple times; only reads captures (or captures nothing) | A pure arrow function with no side effects on captured vars |
Every Fn is also FnMut; every FnMut is also FnOnce. When you take a &dyn Fn(T) -> U parameter, you accept the most restrictive requirement — the closure must be callable multiple times without mutating.
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(f(x))}
fn main() { let double = |x| x * 2; println!("{}", apply_twice(double, 3)); // 12}Async closures
Section titled “Async closures”In TypeScript, async () => { ... } is just a function that returns a promise, and nothing about the syntax changes. In Rust the equivalent was awkward for years: a closure returning async { ... } produces a future that cannot borrow from the closure’s captures, so anything the future touched had to be cloned or moved.
Rust 1.85 added real async closures — async || { ... } — along with the AsyncFn, AsyncFnMut and AsyncFnOnce traits that mirror the three Fn traits above. The future they return is allowed to borrow from the captures, which is what makes the natural code compile.
// TypeScript: async arrow functionconst runTwice = async (f: (url: string) => Promise<number>) => (await f("https://a.example")) + (await f("https://bb.example"));
const log: string[] = [];const total = await runTwice(async (url) => { const n = await fetchLen(url); log.push(`${url} -> ${n}`); // closes over 'log' freely return n;});// Rust: async closure, bound with AsyncFnMutasync fn run_twice<F>(mut f: F) -> usizewhere F: AsyncFnMut(&str) -> usize,{ f("https://a.example").await + f("https://bb.example").await}
let mut log: Vec<String> = Vec::new();let total = run_twice(async |url: &str| { let n = fetch_len(url).await; log.push(format!("{url} -> {n}")); // borrows 'log' across the await n}).await;Take the bound that matches how the closure is used, exactly as with Fn/FnMut/FnOnce: AsyncFn if it only reads its captures, AsyncFnMut if it mutates them (the example pushes to log), AsyncFnOnce if it consumes them.
// @skip-verify crate: tokioasync fn fetch_len(url: &str) -> usize { url.len() // pretend this awaits the network}
// Takes an async closure: AsyncFnMut can mutate what it captures.async fn run_twice<F>(mut f: F) -> usizewhere F: AsyncFnMut(&str) -> usize,{ f("https://a.example").await + f("https://bb.example").await}
#[tokio::main]async fn main() { let mut log: Vec<String> = Vec::new();
let total = run_twice(async |url: &str| { let n = fetch_len(url).await; log.push(format!("{url} -> {n}")); n }) .await;
println!("total = {total}"); for line in &log { println!("{line}"); }}Compiling…
Iterator adapters — lazy by default
Section titled “Iterator adapters — lazy by default”TypeScript’s .map(), .filter(), and .reduce() are eager: they allocate a new array at each step. Rust’s iterator adapters are lazy: nothing is computed until you call a consuming method like .collect(), .sum(), or .for_each(). This means a long chain of .map().filter().take() allocates no intermediate vectors.
// TypeScript — each step allocates a new arrayconst result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] .filter(x => x % 2 === 0) // new array [2,4,6,8,10] .map(x => x * x) // new array [4,16,36,64,100] .slice(0, 3); // new array [4,16,36]console.log(result);fn main() { // Rust — no intermediate allocations until collect() let result: Vec<i32> = (1..=10) .filter(|x| x % 2 == 0) // lazy .map(|x| x * x) // lazy .take(3) // lazy .collect(); // ONE allocation here println!("{:?}", result); // [4, 16, 36]
// Common adapters let sum: i32 = (1..=100).sum(); println!("sum 1..100 = {sum}");
let words = vec!["hello", "world", "rust"]; let upper: Vec<String> = words.iter() .map(|w| w.to_uppercase()) .collect(); println!("{:?}", upper);
// flat_map (like Array.flatMap) let sentences = vec!["hello world", "foo bar"]; let all_words: Vec<&str> = sentences.iter() .flat_map(|s| s.split_whitespace()) .collect(); println!("{:?}", all_words);}Try it
Section titled “Try it”fn main() { let multiplier = 3; let triple = |x| x * multiplier; println!("triple(5) = {}", triple(5));
let numbers = vec![1, 2, 3, 4, 5, 6]; let evens_doubled: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * 2) .collect(); println!("evens_doubled = {:?}", evens_doubled);
let sum: i32 = numbers.iter().sum(); println!("sum = {sum}");
let result: Vec<String> = (1..=3) .map(|x| x * x) .map(|x| format!("sq:{x}")) .collect(); println!("{:?}", result);}Compiling…