π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 andi32
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:
By reference (immutable): The closure borrows the variable immutably.
By reference (mutable): The closure borrows the variable mutably.
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
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
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 implementFnOnce
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 theFn
traits.
Fn
Trait
Fn
Traitfn 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
FnMut
Traitfn 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
FnOnce
Traitfn 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 thenext()
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
Iterator
TraitItem
is the associated type that represents the type of elements the iterator will produce.next
is the core method of theIterator
trait. It advances the iterator and returns the next item in the sequence wrapped in anOption
. If there are no more items, it returnsNone
.
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 theiter
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
collect
methodIn 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
sum
methodIn 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
for_each
methodIn 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
map
methodIn 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
filter
methodIn 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
enumerate
methodIn 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
take
methodIn 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
skip
methodIn 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
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