🌀Closures & Iterators

An comprehensive overview explanation of iterators and closures in Rust.

Closures

What is Closure?

💡 Closures in Rust are anonymous functions that capture and use variables from their enclosing scope. They are often referred to as lambda expressions or anonymous functions in other programming languages.

💡 Rust’s closures allow you to store them in variables, pass them as arguments to other functions, and return them from functions. Furthermore, you can create closures in one place and then call them elsewhere to evaluate them in different contexts.

Closures in Rust are defined using vertical bars | to enclose their parameters, followed by the body of the closure:

let closure = |param1, param2| {
    // closure body
};
fn main() {
    let my_closure = |x: i32| -> i32 { x + 1 };

    let x = my_closure(12);
    println!("x: {x}") // x: 13
}

In this example, we define a closure which is called my_closure. This closure includes:

  • |x: i32|: This part establishes the closure's parameters. x is the parameter name and i32 is its type.

  • > i32: This specifies the return type of the closure.

  • x + 1: This is the closure's body.

We call the closure with the x parameter of 12. The result should be 12 + 1 = 13.

This example also illustrates that a variable can bind to a closure definition, and we can later call the closure by using the variable name and parentheses (my_closure()) similar to when we call a function.

Capturing References or Moving Ownership

Closures can capture variables from their environment (enclosing scope) in three ways:

  1. By reference (immutable): The closure borrows the variable immutably.

  2. By reference (mutable): The closure borrows the variable mutably.

  3. By value: The closure takes ownership of the variable.

fn main() {
    let num: i32 = 5;

    // Capture num by reference (immutable)
    let add_one_v1 = |x: i32| x + num;

    // Capture num by reference (mutable)
    let add_one_v2 = |mut x: i32| {
        x += num;
        x
    };

    let result_v1 = add_one_v1(5);
    let result_v2 = add_one_v2(5);

    println!("result_v1: {result_v1}\\nresult_v2: {result_v2}")
}

// Output
result_v1: 10
result_v2: 10
fn main() {
    // Moving ownership
    let list = vec![1, 2, 3];
    let closure = move || println!("{:?}", list);

    closure(); // [1, 2, 3]

    // `list` is no longer accessible here as it has been moved into the closure.
    // Uncomment to see the error
    // println!("list: {list:?}");
}

Closure Type Inference and Annotation

Closures in Rust typically do not require type annotations for their parameters or return values, unlike fn functions. This is because closures are used within a narrow context, and the compiler can usually infer their types.

Type annotations are necessary for functions because they define an explicit interface, ensuring that all users agree on the types of values a function uses and returns. However, closures are usually short-lived and used in specific, limited contexts, making type inference sufficient in most cases.

In some situations, you may choose to add type annotations to closures for clarity and explicitness, even though it increases verbosity.

There are some variants of the closure syntax:

fn main() {
    // fully annotated closure definition
    let fully_annotated_closure = |x: i32| -> i32 { x + 1 };

    // Remove the type annotations from the closure definition
    // and remove the curly brackets,
    // which are optional because the closure body has only one expression
    let type_annotations_closure = |x| x + 1;

    // multiple parameters
    let multiple_parameters_closure = |x: i32, y: i32| x + y;

    let x = fully_annotated_closure(12);
    let y = type_annotations_closure(12);
    let z = multiple_parameters_closure(5, 5);

    println!("x: {x}, y: {y}, z: {z}")
}

Moving Captured Values Out of Closures and the Fn Traits

💡 Once a closure captures a reference or takes ownership of a value from its defining environment, the code within the closure's body determines how these references or values are used when the closure is executed. This influences what, if anything, is moved into or out of the closure during its execution.

💡 A closure body can perform various actions: it can move a captured value out of the closure, mutate the captured value, leave the value unchanged, or not capture anything from the environment at all.

Rust offers three closure traits to distinguish how captured values are handled:

  • FnOnce: This is the base trait for all closures. It indicates that the closure can be called only once. If the closure moves captured values, it will only implement FnOnce and not the other traits.

  • FnMut: This trait is implemented for closures that don't move captured values out of their body but might mutate them. This allows the closure to be called multiple times with side effects.

  • Fn: This trait is for closures that don’t move or mutate captured values. They can be called multiple times without affecting the captured environment

Functions can also implement all three of the Fn traits. If we don't need to capture a value from the environment, we can use a function name instead of a closure in contexts that require an implementation of one of the Fn traits.

Fn Trait

fn call_fn<F>(closure: F)
where
    F: Fn(i32) -> i32,
{
    println!("Result: {}", closure(5));
}

fn main() {
    let x = 10;
    let closure = |y| y + x;
    call_fn(closure); // Outputs: Result: 15

    // We can call it multiple times without affecting the captured environment
    call_fn(closure); // Outputs: Result: 15
}

FnMut Trait

fn call_fn_mut<F>(mut closure: F)
where
    F: FnMut(i32) -> i32,
{
    println!("Result: {}", closure(5));
    println!("Result: {}", closure(10));
}

fn main() {
    let mut x = 10;
    let closure = |y| {
        x += y; // `x` is captured by mutable reference
        x
    };
    
    call_fn_mut(closure); // Outputs: Result: 15, Result: 25
    println!("x after call_fn_mut: {}", x); // x has been mutated, outputs: 25
}

FnOnce Trait

fn call_fn_once<F>(closure: F)
where
    F: FnOnce(String) -> String,
{
    let result = closure(String::from(", world!"));
    println!("Result: {}", result);
}

fn main() {
    let x = String::from("Hello");
    let closure = move |y: String| x + &y; // `x` is captured by taking ownership

    call_fn_once(closure); // Outputs: Result: Hello, world!

    // println!("x after call_fn_once: {}", x) // `x` is no longer accessible here as it has been moved into the closure
}

Iterators

What is an Iterator?

💡 The Iterator is a trait that allows you to sequentially access the elements of a collection or sequence. It provides a way to process a sequence of elements, one element at a time, and allows us to apply operations such as mapping, filtering, and folding over the sequence.

Rust provides several iterator traits to handle different scenarios:

  • Iterator: The base trait for all iterators. It defines the next() method to retrieve the next element.

  • IntoIterator: Allows converting a value into an iterator.

  • FromIterator: Allows creating a collection from an iterator.

For example, Rust provides a convenient way to iterate over iterators using the for loop:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    for x in &v {
        println!("value: {}", x);
    }
}

// Output
value: 1
value: 2
value: 3
value: 4
value: 5

💡 Iterators are lazy, meaning that they don’t perform any work until they are consumed such as by for loop or collect method. Once an iterator is consumed, it can’t be used again, meaning that you can’t reuse it to iterate over the same element again.

fn main() {
    let v = vec![1, 2, 3];
    let mut iter = v.iter();

    // First iteration
    while let Some(x) = iter.next() {
        println!("value: {}", x);
    }

    // Second iteration (will not print anything)
    while let Some(x) = iter.next() {
        println!("value: {}", x);
    }
}

In this example, the first while loop consumes the iterator by iterating over all elements.

The second while loop tries to iterate again but immediately exits because the iterator is empty.

The Iterator Trait

💡 The Iterator trait in Rust defines a sequence of elements and a way to iterate over them.

  • Item is the associated type that represents the type of elements the iterator will produce.

  • next is the core method of the Iterator trait. It advances the iterator and returns the next item in the sequence wrapped in an Option. If there are no more items, it returns None.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // other methods
}

Many collections in Rust, like arrays, vectors, and ranges, implement the Iterator trait, allowing you to create iterators from them.

You can manually use the next() method to iterate over each element of the iterator at a time.

fn main() {
    let v = vec![1, 2, 3];
    let mut iter = v.iter();

    println!("Value: {:?}", iter.next()); // Some(1)
    println!("Value: {:?}", iter.next()); // Some(2)
    println!("Value: {:?}", iter.next()); // Some(3)
    println!("Value: {:?}", iter.next()); // None
}

Each time we call next method, it consumes an element from the iterator. We didn’t need to make iter mutable when we used a for loop because the loop took ownership of iter and made it mutable behind the scenes.

It's important to note that the values returned by the next method are immutable references to the elements in the vector. This is because the iter method creates an iterator that yields immutable references.

Consuming Iterators

💡 Methods that call next are known as consuming adapters because they will consume the iterator and produce a final value.

Essentially, each time you call one of these methods, you're advancing through the iterator and consuming its elements one at a time. Once an element is consumed, it's gone, and the iterator moves on to the next one.

Some of the common methods that consume the iterator such as: collect(), sum(), for_each(), etc.

The collect method

💡 The collect method converts an iterator into a collection, like a Vec, HashMap, or other types that implement FromIterator.

In this example, we’re calling the map method and applying the closure |x| x * 2 to each element and doubling each number.

After that, we call the collect method to consume the iterator and collect its element into a new vector named doubled.

fn main() {
    let v = vec![1, 2, 3];
    let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();

    println!("After doubled: {:?}", doubled); // After doubled: [2, 4, 6]
}

The sum method

💡 The sum method consumes the iterator and returns the sum of the elements.

In this example, we’re using the sum method to iterate over each element and accumulate the sum of each element in the vector.

fn main() {
    let v = vec![1, 2, 3];
    let total: i32 = v.iter().sum();

    println!("Total sum: {:?}", total); // Total sum: 6
}

The for_each method

💡 The for_each method applies a function to each element of the iterator.

In this example, we’re using the for_each method to iterate over each element and print out each element in the vector.

fn main() {
    let v = vec![1, 2, 3];
    v.iter().for_each(|x| println!("{}", x));

    // Output: 1 2 3
}

Producing Iterators

💡 Iterator adapter is a method provided by the Iterator trait that transforms one iterator into another iterator. These adapters produce new iterators that can be further chained, filtered, or modified without consuming the original iterator or producing a final value immediately. This process is also known as lazy evaluation.

Some of the common methods that produce the iterator such as: map(), filter(), enumerate(), take(), skip(), etc.

The map method

💡 We can use map method to transform each element of an iterator using a custom function that is passed in the map as an argument.

In this example, we’re using the map method to produce a new iterator that multiplies each element by 2.

fn main() {
    let v = vec![1, 2, 3];
    let after_mapping = v.iter().map(|x| x * 2);

    println!("after_mapping: {:?}", after_mapping.collect::<Vec<_>>())
}

// Output: after_mapping: [2, 4, 6]

The filter method

💡 The filter produces a new iterator that includes only the elements that satisfy a given predicate.

In this example, we’re using the filter method to produce a new iterator that only contains even numbers.

fn main() {
    let v = vec![1, 2, 3, 4];
    let after_filtered = v.iter().filter(|&x| x % 2 == 0);

    println!("after_filtered: {:?}", after_filtered.collect::<Vec<_>>())
}

// Output: after_filtered: [2, 4]

The enumerate method

💡 The enumerate method produces an iterator of pairs, where each pair contains the index and the value of the element.

In this example, we’re using the enumerate method to produce an iterator that yields (index, value) pairs.

fn main() {
    let v = vec!["a", "b", "c"];
    let after_enumerated = v.iter().enumerate();

    println!(
        "after_enumerated: {:?}",
        after_enumerated.collect::<Vec<_>>()
    )
}

// Output: after_enumerated: [(0, "a"), (1, "b"), (2, "c")]

The take method

💡 The take method produces an iterator that yields only the first n elements of the original iterator.

In this example, we’re using the take method to produce an iterator that returns only the first 3 elements.

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let after_taken = v.iter().take(3);

    println!("after_taken: {:?}", after_taken.collect::<Vec<_>>())
}

// Output: after_taken: [1, 2, 3]

The skip method

💡 The skip method produces an iterator that skips the first n elements and yields the rest.

In this example, we’re using the skip method to produce an iterator that skips the first two elements and returns the rest.

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let after_skipped = v.iter().skip(2);

    println!("after_skipped: {:?}", after_skipped.collect::<Vec<_>>())
}

// Output: after_skipped: [3, 4, 5]

Chaining Producing Adapters

💡 The powerful feature of producing an adapter is that it allows us to combine multiple iterator adapters to create a complex data transformation. This process is called chaining-producing adapters.

💡 By chaining these adapters, you can perform multiple operations on a data stream without creating intermediate collections.

Let’s see an example of chaining multiple producing adapters by combining all of the methods that we’ve been through in the previous section.

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6];

    // Chaining producing adapters
    let result: Vec<i32> = v
        .iter()
        .filter(|&&x| x % 2 == 0) // Keep only even numbers
        .map(|&x| x * 10)         // Multiply each by 10
        .take(2)                  // Take only the first 2 elements
        .collect();               // Collect the results into a vector

    println!("{:?}", result);     // Outputs: [20, 40]
}

We create a vector named v which contains numbers from 1 to 6.

We call the iter() method on the vector v, creating an iterator over its elements. This iterator yields references to the elements of the vector.

In the first stage, we want to filter out to keep only even numbers. The filter method is applied to the iterator, creating a new iterator that yields only the elements that satisfy the given predicate, in this case is the even numbers.

In the second stage, we want to multiply each element by 10. The map method is applied to the filtered iterator, creating a new iterator that multiplies each element by 10.

In the third stage, we only want to take the first 2 elements from the previous result. The take method is applied to the iterator, creating a new iterator that yields at most 2 elements.

Finally, we call the collect method to consume the iterator and collect its elements into a new vector.

We can see that each adapter creates a new iterator, transforming the data in a specific way using the result produced by the previous method.

Last updated