Functional Language Features: Closures and Iterators
Auto's design takes inspiration from many languages, and one significant influence is functional programming. Programming in a functional style often includes using functions as values by passing them in arguments, returning them from other functions, and assigning them to variables for later execution.
In this chapter, we'll cover:
- Closures, anonymous functions you can store in a variable
- Iterators, a way of processing a series of elements
- How to use closures and iterators to improve the I/O project from Chapter 12
- Performance of closures and iterators
Closures
Closures are anonymous functions you can save in a variable or pass as arguments to other functions. Unlike regular functions, closures can capture values from the scope in which they're defined.
Closure Syntax
Auto closures use (params) => expr syntax:
let add = (a int, b int) => a + b
let result = add(5, 3)
print(result)let add = |a: i32, b: i32| a + b;
let result = add(5, 3);
println!("{}", result);This is equivalent to Rust's |params| expr syntax. Closures can also have explicit return types:
let expensive_closure = (num int) -> int {
print("calculating slowly...")
num
}let expensive_closure = |num: i32| -> i32 {
println!("calculating slowly...");
num
};Capturing the Environment
Closures can capture values from their environment. Here's an example using an unwrap_or_else call with a closure that captures self:
The closure (self) => self.most_stocked() captures an immutable reference to self and calls most_stocked(). The unwrap_or_else method on ?T (Option) calls the closure only when the value is None.
Type Inference
Closures don't usually require type annotations — the compiler infers them from usage:
The compiler infers the type of x as String from the first call. Once inferred, the closure's type is locked — calling it with a different type would be a compile error.
Capturing References or Moving Ownership
Closures can capture values in three ways, which map to how functions take parameters:
- Immutable reference — when the closure only reads the value
- Mutable reference — when the closure modifies the value
- Ownership — when the closure takes ownership (using
movein Rust)
Here's a closure that captures an immutable reference:
The closure only_borrows captures list by immutable reference. The original list remains accessible before, during, and after the closure call.
And a closure that captures a mutable reference:
When borrows_mutably captures a mutable reference, no other access to list is allowed between the closure definition and the call.
Fn Traits
Like Rust, closures implement one or more of the Fn traits:
FnOnce— can be called once (moves captured values out)FnMut— can be called multiple times, may mutate captured valuesFn— can be called multiple times without mutation
These traits are important when passing closures to functions or storing them in types.
Iterators
Iterators provide a way to process a sequence of elements. In Auto, the for loop is the primary way to iterate:
for item in collection {
print(item)
}for item in &collection {
println!("{}", item);
}The for Loop
The for loop works with arrays, lists, ranges, and any type that implements the iterator pattern:
// Iterate over an array
let arr = [1, 2, 3]
for n in arr {
print(n)
}
// Iterate over a range
for i in 0..10 {
print(i)
}let arr = [1, 2, 3];
for n in arr {
println!("{}", n);
}
for i in 0..10 {
println!("{}", i);
}Under the hood, for loops use an iterator. The loop takes ownership of the iterator and calls next() repeatedly until it returns None.
Iterator Methods
Note: The following iterator methods are standard Rust
Iteratortrait methods. When using Auto withuse.rust, you can access these methods directly on types that implementIterator.
Creating Iterators
| Method | Description | Ownership |
|---|---|---|
.iter() | Iterates over references | Borrows each element |
.iter_mut() | Iterates over mutable references | Mutably borrows each element |
.into_iter() | Iterates over owned values | Takes ownership |
Consuming Adapters
Consuming adapters call next() and use up the iterator:
// sum — consumes the iterator and returns the total
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum(); // 6Iterator Adapters
Iterator adapters transform iterators without consuming them. They're lazy — they don't do anything until a consuming adapter is called:
// map — transforms each element (lazy!)
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
// v2 is [2, 3, 4]
// filter — keeps elements matching a predicate (lazy!)
let shoes: Vec<Shoe> = shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect();Using Closures with Iterator Methods
Iterator methods commonly take closures as arguments. Here's an example using filter with a closure that captures shoe_size from its environment:
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}The closure |s| s.size == shoe_size captures shoe_size from the surrounding scope and compares it with each shoe's size.
Improving Our I/O Project
With closures and iterators, we can improve the minigrep project from Chapter 12.
Refactoring search with Iterators
The original search function used a mutable results vector:
fn search(query String, contents String) []String {
var results = List.new()
for line in contents.lines() {
if line.contains(query) {
results.push(line)
}
}
results
}Using iterator adapters, we can make this more concise and avoid the mutable intermediate vector:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}This uses filter to keep only lines containing the query, then collect to gather them into a vector. The functional style minimizes mutable state and makes the code easier to understand.
Closures and Iterators Quick Reference
| Feature | Auto | Rust |
|---|---|---|
| Closure syntax | (x) => x + 1 | |x| x + 1 |
| Closure with types | (x int) -> int { x + 1 } | |x: i32| -> i32 { x + 1 } |
| For loop | for x in list { ... } | for x in &list { ... } |
| Range | 0..10 | 0..10 |
| Filter | .filter((s) => s.size == 10) | .filter(|s| s.size == 10) |
| Map | .map((x) => x + 1) | .map(|x| x + 1) |
| Collect | .collect() | .collect() |
| Sum | .sum() | .sum() |
Summary
This chapter covered two key functional programming features:
Closures — anonymous functions that capture their environment
(params) => exprsyntax- Type inference from usage
- Capture by immutable reference, mutable reference, or ownership
Fn,FnMut,FnOncetraits
Iterators — lazy sequences that process elements one at a time
forloop as the primary iteration mechanism- Consuming adapters (
.sum()) and iterator adapters (.map(),.filter()) - Chaining adapters for complex transformations
I/O project improvements — using iterator adapters to simplify
search
Closures and iterators are zero-cost abstractions in Rust — they compile to the same code as hand-written loops. Auto inherits this performance model.
In the next chapter, we'll explore more about Auto's smart pointer types and how they manage memory.