Code Story

Chapter #6 – Lambdas & Closures

anonymous functions, environment capture, closure types, function pointers

6.0  Prologue

A lambda (anonymous function) is a function defined inline without a name. A closure is a lambda that captures variables from its enclosing scope. Closures enable passing behavior as data, deferring computation, and building iterators and callbacks without polluting the namespace with single-use named functions.

6.1  Anonymous Functions

Each language has a distinct syntax for inline function literals: // Rust |params| body or |params| { block } let double = |x: i32| x * 2; let add = |a, b| { a + b }; // types inferred from usage // C++ [capture](params) -> ret { body } auto double = [](int x) { return x * 2; }; auto add = [](int a, int b) -> int { return a + b; }; // C# (params) => expr or (params) => { block } Func<int,int> double = x => x * 2; Func<int,int,int> add = (a, b) => a + b; // Python lambda params: expr (expression-only body) double = lambda x: x * 2 add = lambda a, b: a + b Python lambdas are restricted to a single expression; multi-statement logic requires a named function. The other three languages allow full block bodies in their lambda forms.

6.2  Capturing Environment

A closure captures variables from the enclosing scope. The capture mode determines whether the closure borrows, copies, or moves the captured value. // Rust: capture mode inferred; move forces ownership transfer let threshold = 10; let above = |x| x > threshold; // borrows threshold let msg = String::from("hello"); let print_msg = move || println!("{msg}"); // moves msg into closure // C++: capture list controls the mode int threshold = 10; auto above = [threshold](int x) { return x > threshold; }; // copy auto above = [&threshold](int x) { return x > threshold; }; // reference auto above = [=](int x) { return x > threshold; }; // all by copy auto above = [&](int x) { return x > threshold; }; // all by ref // C# and Python: always capture by reference (the variable binding, not the value) int threshold = 10; Func<int,bool> above = x => x > threshold; // C#: captures the variable threshold = 20; // above now uses 20 Rust’s borrow checker enforces that captured references do not outlive the closure. C++ offers no such guarantee - a closure capturing a reference to a local variable and escaping the local’s scope is undefined behavior.

6.3  Closure Types

Rust models closure capabilities as three traits:
Trait Meaning Can be called
Fn borrows captures immutably any number of times
FnMut borrows captures mutably any number of times (requires mut)
FnOnce consumes captured values exactly once
Every closure implements at least FnOnce. If it does not consume its captures it also implements FnMut; if it does not mutate them it also implements Fn. Function parameters accepting closures use these as bounds: fn apply(f: impl Fn(i32) -> i32). In C++, lambdas are anonymous struct types with an operator(). They are not standardly polymorphic across different lambda types; std::function<T> provides a type-erased wrapper at the cost of a heap allocation and virtual dispatch. In C#, delegates and Func<>/Action<> serve the same role. Python functions are first-class objects and are inherently polymorphic.

6.4  Function Pointers vs Closures

A function pointer holds the address of a named function (no captured state). It is a single pointer-size value and incurs no heap allocation. A closure may carry captured state alongside the code pointer. // Rust fn square(x: i32) -> i32 { x * x } let f: fn(i32) -> i32 = square; // function pointer, no capture let g = |x: i32| x * x; // closure (zero-capture, also fn-pointer compatible) // C++ int (*f)(int) = square; // C-style function pointer auto g = [](int x) { return x*x; }; // non-capturing lambda (convertible to fn ptr) // C# Func<int,int> f = x => x * x; // delegate; always heap-allocated In Rust, a non-capturing closure is zero-sized and can be coerced to a function pointer. A capturing closure has non-zero size and cannot. Using impl Fn (static dispatch, monomorphized) avoids heap allocation; using Box<dyn Fn> (dynamic dispatch) allows heterogeneous collections of closures.

6.5  Epilogue

Closures connect functions to the state they need without global variables or extra parameters. Understanding capture modes prevents dangling-reference bugs in C++ and unexpected mutation in C# and Python. The next chapter addresses how programs signal and recover from failures.

6.6  References

Rust Closures - The Book
C++ Lambda Expressions - cppreference
C# Lambda Expressions - Microsoft
Python Lambda Expressions