Skip to content
📖 Auto vs RustBook contents

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:

auto
let add = (a int, b int) => a + b
let result = add(5, 3)
print(result)
rust
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:

auto
let expensive_closure = (num int) -> int {
    print("calculating slowly...")
    num
}
rust
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:

Closure capturing `self` from the environment

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:

Closure type inference

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:

  1. Immutable reference — when the closure only reads the value
  2. Mutable reference — when the closure modifies the value
  3. Ownership — when the closure takes ownership (using move in Rust)
Closure with explicit type annotations

Here's a closure that captures an immutable reference:

Closure capturing 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:

Closure capturing 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 values
  • Fn — 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:

auto
for item in collection {
    print(item)
}
rust
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:

auto
// 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)
}
rust
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 Iterator trait methods. When using Auto with use.rust, you can access these methods directly on types that implement Iterator.

Creating Iterators ​

MethodDescriptionOwnership
.iter()Iterates over referencesBorrows each element
.iter_mut()Iterates over mutable referencesMutably borrows each element
.into_iter()Iterates over owned valuesTakes ownership

Consuming Adapters ​

Consuming adapters call next() and use up the iterator:

rust
// sum — consumes the iterator and returns the total
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();  // 6

Iterator Adapters ​

Iterator adapters transform iterators without consuming them. They're lazy — they don't do anything until a consuming adapter is called:

rust
// 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:

rust
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:

auto
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:

rust
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 ​

FeatureAutoRust
Closure syntax(x) =&gt; x + 1|x| x + 1
Closure with types(x int) -&gt; int { x + 1 }|x: i32| -&gt; i32 { x + 1 }
For loopfor x in list { ... }for x in &list { ... }
Range0..100..10
Filter.filter((s) =&gt; s.size == 10).filter(|s| s.size == 10)
Map.map((x) =&gt; x + 1).map(|x| x + 1)
Collect.collect().collect()
Sum.sum().sum()

Summary ​

This chapter covered two key functional programming features:

  1. Closures — anonymous functions that capture their environment

    • (params) =&gt; expr syntax
    • Type inference from usage
    • Capture by immutable reference, mutable reference, or ownership
    • Fn, FnMut, FnOnce traits
  2. Iterators — lazy sequences that process elements one at a time

    • for loop as the primary iteration mechanism
    • Consuming adapters (.sum()) and iterator adapters (.map(), .filter())
    • Chaining adapters for complex transformations
  3. 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.

Released under the MIT License.