Closures & Iterators
In Chapter 3 we introduced functions as one of Auto's fundamental building blocks. Functions let you name a reusable piece of logic and call it by name. But sometimes you need something more flexible -- a function you can store in a variable, pass as an argument, or return from another function. That is what closures provide.
Once you have closures, a powerful pattern emerges: processing sequences of elements by transforming, filtering, and reducing them with small, composable functions. That is what iterators provide.
Together, closures and iterators form the backbone of functional programming in Auto. This chapter covers both.
Closures: Functions as Values
A closure is an anonymous function -- a function without a name. You can store it in a variable, pass it as an argument to another function, or return it from a function. The word "closure" comes from the fact that such a function can close over (capture) variables from the scope in which it is defined.
Closure Syntax
Auto uses fn(params) returnType { body } syntax for closures. This is the same fn keyword used for named functions, but without a name:
Let us walk through the key ideas:
fn apply(fn fn(int)int, x int) int-- The parameter typefn(int)intmeans "a function that takes anintand returns anint." Thefnkeyword before the parameter name is part of the type syntax -- it tells the compiler this parameter is a function, not a regular value. Inside the function body,fn(x)calls the closure with argumentx.var double = fn(x int) int { x * 2 }-- This creates a closure and stores it in the variabledouble. The closure takes oneintparameterxand returnsx * 2. The variabledoublenow holds a function value that you can call likedouble(5).apply(double, 5)-- Passes thedoubleclosure as the first argument toapply. Insideapply, the closure is called with5, producing10. The same pattern works foradd_one, which adds 1 to its argument.
This pattern -- passing a function to another function -- is called a higher-order function. The apply function is higher-order because it takes a function as a parameter. Higher-order functions are the foundation of functional programming.
In Rust, closures use the |params| body syntax. In Python, they use lambda. In C, they become function pointers (for simple cases) or structs with function pointers and captured state. In TypeScript, they are arrow functions.
Capturing Variables
The real power of closures is capturing -- the ability to use variables from the surrounding scope inside the closure body. When a closure captures a variable, it "remembers" that variable even after the surrounding scope has exited.
Here is what is happening:
fn make_multiplier(factor int) fn(int)int-- This function takes anintparameterfactorand returns a closure of typefn(int)int. The return typefn(int)inttells the compiler: "this function returns a function that takes oneintand returns anint."fn(n int) int { n * factor }-- This is the closure being returned. It capturesfactorfrom the surrounding scope (themake_multiplierfunction's parameter). Even aftermake_multiplierreturns, the closure retains access tofactor.var triple = make_multiplier(3)-- Callsmake_multiplierwithfactor = 3. The returned closure "remembers" thatfactoris3. Every call totriple(n)computesn * 3.triple(5)produces15, andtriple(10)produces30. Both calls use the captured valuefactor = 3.
This pattern is sometimes called a factory function -- a function that creates and returns other functions, each customized with different captured state. You could call make_multiplier(2) to get a doubling function, or make_multiplier(10) to get a function that multiplies by ten. Each returned closure is independent.
In Rust, the move keyword transfers ownership of captured variables into the closure. In Python, inner functions naturally capture variables from enclosing scopes. In C, the closure is represented as a struct containing both the captured state (factor) and a function pointer. In TypeScript, inner arrow functions capture variables from the outer function through lexical scoping.
Iterators: Lazy Sequences
An iterator is an object that produces a sequence of values, one at a time. You have already used iterators implicitly through for loops (Chapter 3) and ranges (Chapter 2). Now let us see how they work under the hood.
.iter() and .next()
Every collection in Auto can produce an iterator via the .iter() method. An iterator has a .next() method that returns the next element each time you call it. When there are no more elements, .next() returns None.
Key observations:
var iter = numbers.iter()-- Creates an iterator over the array[1, 2, 3, 4, 5]. The iterator does not copy the data -- it holds a reference (or pointer) to the original array and a position counter.iter.next()-- ReturnsSome(1)the first time,Some(2)the second time, and so on. After the last element, it returnsNone. Each call advances the internal position by one.The iterator is lazy -- It does not process all elements up front. It only produces the next value when you ask for it. This laziness is what makes chaining iterator methods efficient.
The for loop you have been using since Chapter 3 is syntactic sugar over .iter() and .next(). When you write for n in numbers { ... }, Auto internally creates an iterator and calls .next() in a loop until it gets None.
In Rust, .iter() returns Option<&T>, so println!("{:?}", iter.next()) prints Some(1), Some(2), etc. In Python, next() raises StopIteration after the last element (or you can use a for loop which handles this automatically). In C, the iterator is implemented as a struct with a pointer and position, returning NULL when exhausted. In TypeScript, generators provide the same interface with .next() returning { value, done }.
Transformation: .map()
The .map() method transforms each element in a sequence by applying a closure to it. It produces a new sequence with the transformed values.
Here is what happens step by step:
numbers.map(fn(x) int { x * 2 })-- The.map()method takes a closure and applies it to each element of the array. The closurefn(x) int { x * 2 }takes one parameterx(type inferred asintfrom the array element type) and returnsx * 2. The result is a new array[2, 4, 6, 8, 10].for n in doubled-- Iterates over the resulting array and prints each element.
Notice that the closure parameter x does not need an explicit type annotation. Auto infers it from the array element type. You could write fn(x int) int { x * 2 } with an explicit type, but it is not required. This is the same type inference principle that applies to closures in Rust and TypeScript.
In Python, .map() transpiles to a list comprehension, which is the idiomatic Python equivalent. In C, .map() becomes a simple for loop that fills a new array. In TypeScript, .map() is a native Array method.
Filtering: .filter()
The .filter() method keeps only the elements that satisfy a predicate -- a closure that returns bool. Elements for which the predicate returns false are discarded.
Key ideas:
numbers.filter(fn(x int) bool { x % 2 == 0 })-- The.filter()method takes a predicate closure. For each element, it calls the predicate. If the predicate returnstrue, the element is kept. Iffalse, the element is discarded. The result is a new array[2, 4, 6, 8, 10].The predicate closure --
fn(x int) bool { x % 2 == 0 }takes anintand returns abool. The expressionx % 2 == 0istruewhenxis even andfalsewhenxis odd..filter()does not modify the original array -- It produces a new array containing only the elements that pass the filter. The originalnumbersarray remains unchanged.
In Rust, .filter() returns an iterator adapter that is lazy -- it does not allocate a new vector until .collect() is called. In Python, the equivalent is a list comprehension with an if clause. In C, the filter is implemented as a loop that copies matching elements into a new array. In TypeScript, .filter() is a native Array method.
Reduction: .fold()
The .fold() method reduces a sequence to a single value by repeatedly applying a closure that combines an accumulator with each element. It takes two arguments: an initial value for the accumulator, and a closure that takes the current accumulator and the current element, and returns the new accumulator.
Let us break this down:
numbers.fold(0, fn(acc int, x int) int { acc + x })-- The first argument0is the initial value of the accumulator. The closure takes two parameters:acc(the running total so far) andx(the current element). It returnsacc + x, which becomes the new accumulator. The fold processes each element in order:- Start with
acc = 0 acc = 0 + 1 = 1acc = 1 + 2 = 3acc = 3 + 3 = 6acc = 6 + 4 = 10acc = 10 + 5 = 15- Final result:
15
- Start with
numbers.fold(1, fn(acc int, x int) int { acc * x })-- Same pattern, but starts with1and multiplies:1 * 1 * 2 * 3 * 4 * 5 = 120.The initial value matters -- For sum, the initial value is
0(the additive identity). For product, it is1(the multiplicative identity). Choosing the wrong initial value produces wrong results.
.fold() is sometimes called reduce or accumulate in other languages. It is the most general iterator method -- you can implement .map() and .filter() in terms of .fold(), though that would be less readable.
In Python, .fold() maps to functools.reduce. In C, it becomes a for loop with an accumulator variable. In TypeScript, .fold() maps to Array.reduce.
Chaining
The real power of iterators emerges when you chain multiple methods together. Each method returns a new iterator (or a collection), so you can pipe the output of one method into the input of the next. This creates a pipeline that describes a computation as a series of transformations.
Here is how the pipeline works, step by step:
Start:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(fn(x int) bool { x % 2 == 0 })-- Keep only even numbers:[2, 4, 6, 8, 10].map(fn(x int) int { x * x })-- Square each number:[4, 16, 36, 64, 100].fold(0, fn(acc int, x int) int { acc + x })-- Sum them up:4 + 16 + 36 + 64 + 100 = 220print(result)-- Outputs220.
The pipeline reads naturally from left to right: "take these numbers, filter the evens, square them, and sum the result." Each step is a small, self-contained transformation. Compare this to the imperative equivalent -- a loop with a mutable accumulator and conditional logic -- and you can see why the functional style is often clearer for data transformations.
In Rust, .filter() and .map() are lazy iterator adapters. They do not allocate intermediate vectors. Only .fold() (a consuming adapter) forces evaluation. This means the Rust version processes each element through the entire pipeline one at a time, with zero intermediate allocations. In Python, list comprehensions create intermediate lists. In C, the compiler can optimize the single loop into efficient machine code. In TypeScript, .filter() and .map() return new arrays, while .reduce() produces the final value.
Summary
This chapter covered two pillars of functional programming in Auto:
| Concept | Syntax | Purpose |
|---|---|---|
| Closure | fn(x int) int { x * 2 } | Anonymous function as a value |
| Higher-order function | fn apply(fn fn(int)int, ...) | Function that takes/returns functions |
| Capturing | fn(n int) int { n * factor } | Closure uses surrounding variables |
| Iterator | .iter() | Create a sequence producer |
| Next element | .next() | Get the next value from an iterator |
| Transform | .map(fn(x) int { ... }) | Apply a function to each element |
| Filter | .filter(fn(x int) bool { ... }) | Keep elements matching a predicate |
| Reduce | .fold(init, fn(acc, x) int { ... }) | Collapse sequence to a single value |
| Chain | .filter(...).map(...).fold(...) | Compose transformations into a pipeline |
Key takeaways:
Closures are functions as values -- You can store them in variables, pass them as arguments, and return them from other functions. They capture variables from their enclosing scope.
Higher-order functions enable abstraction --
apply,make_multiplier, and the iterator methods are all higher-order functions. They separate the what (the transformation) from the how (the iteration logic).Iterators are lazy --
.iter()creates a lightweight cursor..filter()and.map()do nothing until a consuming adapter like.fold()forces evaluation. This makes chaining efficient.Chaining creates readable pipelines --
.filter().map().fold()reads like a sentence: "filter, then transform, then reduce." Each step is a small, testable, composable unit.The same patterns work across all targets -- Closures and iterators are universal programming concepts. Auto transpiles them to idiomatic equivalents in Rust (closures and
Iteratortrait), Python (lambdas and comprehensions), C (function pointers and loops), and TypeScript (arrow functions andArraymethods).
Closures and iterators are zero-cost abstractions in Rust -- they compile to the same machine code as hand-written loops. Auto inherits this performance model. You get expressive, readable code without sacrificing speed.
In the next chapter, we will explore comptime -- Auto's compile-time metaprogramming system, which lets you execute code at compile time to generate types, values, and even entire functions.