Skip to main content

Closures & Lambdas

Closures are functions that capture variables from their surrounding scope. Here's how different languages handle closures, lambdas, and higher-order functions.

Closures & Anonymous Functions​

// Arrow functions (closures)
const add = (a, b) => a + b;
const square = x => x * x;

// Capturing variables
function counter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const c = counter();
c.increment(); // 1
c.increment(); // 2
c.getCount(); // 2

// IIFE (Immediately Invoked)
const result = ((x) => x * 2)(5); // 10

// Closures in loops
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
// Prints 0, 1, 2 (let creates new scope)
}

Higher-Order Functions​

// Map, filter, reduce
const nums = [1, 2, 3, 4, 5];

nums.map(x => x * 2); // [2, 4, 6, 8, 10]
nums.filter(x => x % 2 === 0); // [2, 4]
nums.reduce((acc, x) => acc + x, 0); // 15

// Function composition
const compose = (f, g) => x => f(g(x));
const double = x => x * 2;
const addOne = x => x + 1;
const doubleThenAdd = compose(addOne, double);
doubleThenAdd(3); // 7

// Currying
const curry = fn => a => b => fn(a, b);
const add = curry((a, b) => a + b);
const add5 = add(5);
add5(3); // 8

Key Takeaways​

  • Capture semantics -- C++ uses [=] for by-value and [&] for by-reference capture; PHP uses use ($x) for by-value and use (&$x) for by-reference. Rust distinguishes Fn (immutable borrow), FnMut (mutable borrow), and FnOnce (takes ownership). For example, let mut count = 0; let inc = || { count += 1; count }; in Rust requires FnMut because the closure mutates count. Choose explicit capture when you need to control lifetime and mutability; Rust's traits enforce this at compile time and prevent accidental moves or borrow conflicts.

  • Closure types in Rust -- Rust's Fn, FnMut, and FnOnce traits encode capture semantics at compile time. A closure that only reads captured variables implements Fn; one that mutates them implements FnMut; one that consumes them (e.g. move) implements FnOnce. When accepting closures as parameters, use impl Fn(i32) -> i32 or F: Fn(i32) -> i32 to constrain what the caller can pass. This matters because FnOnce can only be called once, so it can't be used in a loop. If you write closures that capture state, understand which trait your closure implements to avoid ownership errors.

  • Lambda limitations in C -- C has no closures or lambdas; only function pointers (int (*op)(int, int)). To simulate "closures," you must pass a context struct manually: typedef struct { int factor; } Context; int apply(int x, Context* ctx) { return x * ctx->factor; }. This is verbose and error-prone. For callback-heavy code in C, consider passing a void* context alongside the function pointer; for new projects, prefer languages with native closures (Go, Rust, C++) if you need capture semantics.

  • Java's effectively final -- Java lambdas can only capture variables that are final or effectively final (never reassigned). For example, int factor = 3; Function<Integer, Integer> mul = x -> x * factor; works, but factor = 4; afterward would be a compile error. This prevents mutation of captured state and simplifies the model but limits closures that need to maintain mutable state. For immutable or stateless captures, Java lambdas are fine; for mutable state, use a single-element array or AtomicInteger as a workaround, or prefer a language with full closure capture (e.g. JavaScript, C#).

  • Higher-order functions and built-ins -- JavaScript, Python, Ruby, C#, and Swift have built-in map, filter, reduce (or equivalents). Go does not; you must implement them or use a library. C uses qsort with a comparator function pointer. Rust uses iterator methods (.map(), .filter(), .fold()). Zig uses function pointers and manual loops. If you rely heavily on functional pipelines, choose a language with rich iterator/collection APIs; for minimal abstraction, Go or C style is acceptable.

  • When to choose which -- For web and event-driven code, JavaScript closures are idiomatic. For systems with safety and performance, Rust closures with Fn/FnMut/FnOnce balance expressiveness and control. For enterprise Java, lambdas and streams work well within the "effectively final" constraint. For C/C++/Zig, function pointers are the norm; use closures only when you have C++ or Rust available. Match closure support to your use case: callback-heavy vs. predominantly procedural vs. functional pipelines.