In this lesson, we will introduce Closures in Rust, a flexible feature that allows functions to capture variables from their environment, making them highly useful for tasks like functional programming, callbacks, and lazy evaluation.
What Are Closures?
Closures are blocks of code that can be assigned to variables, passed to other functions, or returned from functions. They are similar to regular functions, but closures have one key advantage: they can capture variables from the scope in which they are defined. This allows them to be more flexible and powerful than traditional functions.
If you prefer a video version:
All the code is available on Github (link in the video description)
Key Characteristics of Closures in Rust:
- Anonymous Functions: Closures are unnamed functions that can be stored in variables or passed to other functions.
- Capturing Environment: Closures can capture values from their surrounding scope by borrowing, mutably borrowing, or taking ownership of them.
- Type Inference: Rust infers the types of parameters and return types in most closures, so explicit type annotations are often unnecessary.
-
Flexibility: Closures can be stored as function pointers or as traits like
Fn
,FnMut
, andFnOnce
, depending on how they capture variables.
Syntax
Closures in Rust are defined using the |args| body
syntax. The args
represent the arguments the closure takes, and the body
is the code that the closure executes. Here's an example of a closure that takes no arguments and returns a string:
let closure = || "Hello, world!";
Here's a more complex example with two arguments:
let closure = |a: i32, b: i32| -> i32 {
a + b
};
Rust can infer the types of a and b in most cases, so you can simplify this to:
let closure = |a, b| a + b;
Example: Simple Closure
Here's an example of a closure that adds two numbers:
fn main() {
let add = |a, b| a + b;
let result = add(3, 5);
println!("The sum is: {}", result);
}
In this example, the closure add takes two arguments, a and b, and returns their sum. The |a, b| syntax defines the closure's arguments, and the body is simply a + b.
Capturing Variables with Closures
Closures can capture variables from their environment in three different ways: by borrowing (immutably or mutably) or by taking ownership of the variable (moving it). This behavior is determined by how the closure is used.
1. Borrowing (Immutable Capture)
In this case, the closure borrows a variable immutably, meaning it can read the variable but not modify it.
fn main() {
let x = 5;
let print_x = || {
println!("The value of x is: {}", x);
};
print_x();
}
In this example, the closure closure borrows the variable x immutably, allowing it to read x but not modify it.
2. Mutably Borrowing (Mutable Capture)
The closure borrows a variable mutably, allowing it to both read and modify the variable.
Example:
fn main() {
let mut y = 100;
let mut print_y = || {
y += 1;
println!("The value of x is: {}", y);
};
print_y();
}
In this example, the closure mutably borrows the variable x, allowing it to modify the value of x.
3. Moving (Ownership Capture)
The closure can take ownership of a variable, meaning the variable is moved into the closure and can no longer be used outside of it.
Example:
fn main() {
let x = String::from("Hello");
let consume_x = move || { // `x` is moved into the closure
println!("Consumed: {}", x);
drop(x); // Consumes `x` by dropping it
};
consume_x(); // `x` is moved and consumed
// consume_x(); // Error: closure can only be called once, `x` was consumed
}
In this example, the closure consume_x takes ownership of the variable x by using the move keyword. After being moved into the closure, x cannot be used outside the closure.
Closure Traits: Fn, FnMut, and FnOnce
Rust provides three traits that represent how closures capture variables from their environment:
- Fn: The closure captures variables immutably, allowing it to be called multiple times.
- FnMut: The closure captures variables mutably, meaning it can modify them. It can also be called multiple times.
- FnOnce: The closure takes ownership of captured variables and can only be called once because it consumes the environment.
Rust automatically chooses the most appropriate trait depending on how the closure is used.
Closures as Function Parameters
Closures can be passed as arguments to functions, enabling custom behavior within those functions.
This is often used in scenarios where you want to pass logic (in the form of a closure) to a function that can then execute it.
Example:
// Fn trait bound: takes an i32 and returns an i32
fn apply<F>(f: F) where F: Fn(i32) -> i32, {
let result = f(10); // Call the closure with 10 as an argument
println!("Result: {}", result);
}
fn main() {
let double = |x| x * 2;
apply(double); // Pass the closure to a function
}
In this example, the apply function accepts a closure f as an argument.
The closure must implement the Fn(i32) -> i32 trait, which means it takes an i32 as input and returns an i32.
The double closure doubles its argument and is passed to apply, which calls it with the value 10.
Differences Between Functions and Closures
While closures and functions share similarities, they have important differences:
Capturing Variables: Closures can capture variables from the surrounding scope, whereas functions cannot.
Syntax: Closures are defined using the |args| body syntax, while functions use the fn keyword.
Flexibility: Closures can be stored in variables, passed around as arguments, and returned from other functions, giving them more flexibility than traditional functions.
Memory Usage: Closures that capture variables from their environment may use more memory than regular functions because they store those captured values.
Conclusion
Closures in Rust are a powerful feature that allows you to capture variables from their environment and create flexible, reusable code blocks. By understanding the different ways closures capture variables (borrowing, mutably borrowing, or moving), you can make the most of Rust's closures in scenarios where traditional functions might fall short.
Closures are often used in functional programming, callback functions, and iterator chains, making them a vital part of mastering Rust.
If you prefer a video version:
All the code is available on Github (link in the video description)
If you have any comments, just drop them below.
You can also find me here: https://francescociulla.com
Top comments (0)