loudsilence
5 min readFeb 23, 2024

--

Support me by buying an item from my wishlist, visiting my reviews site or buy me a coffee!

Understanding Closures in Rust

Rust is a programming language that emphasizes performance, safety, and concurrency. One of the features that makes Rust powerful and expressive is closures, which are anonymous functions that can capture and use variables from their defining environment. In this blog post, we will explore what closures are, how they work, and how they can help us write more flexible and elegant code.

What are closures?

Closures are functions without a name that can be stored in a variable or passed as an argument to other functions. They are defined using a vertical bar (|) and parentheses, similar to regular function syntax. For example, this is a closure that takes one parameter and returns its square:

let square = |x| x * x;

We can call this closure like any other function:

println!("The square of 3 is {}", square(3)); // prints 9

Unlike functions, closures can capture values from the scope in which they are defined. This means they can access and use variables that are not passed as parameters. For example, this closure captures the variable factor from its environment and multiplies it with its parameter:

let factor = 2;
let multiply = |x| x * factor;
println!("2 times 3 is {}", multiply(3)); // prints 6

Closures can capture variables by value or by reference, depending on how they are declared. By default, closures capture variables by reference, using the & operator. This allows the closure to borrow the variable and use it without taking ownership. However, this also means that the variable must remain valid for as long as the closure is used, and that the closure cannot outlive its environment. For example, this code will not compile because the closure tries to use a reference to a variable that has been dropped:

let multiply;
{
let factor = 2;
multiply = |x| x * factor; // closure captures factor by reference
} // factor goes out of scope and is dropped here
println!("2 times 3 is {}", multiply(3)); // error: borrow of moved value: `factor`

To fix this, we can use the move keyword to force the closure to capture variables by value. This means the closure will take ownership of the variables and store them in its own environment. This way, the closure can be used independently of its original scope, as long as it satisfies the ownership and borrowing rules of Rust. For example, this code will compile because the closure owns a copy of factor:

let multiply;
{
let factor = 2;
multiply = move |x| x * factor; // closure captures factor by value
} // factor goes out of scope but is not dropped because it is moved
println!("2 times 3 is {}", multiply(3)); // prints 6

Why use closures?

Closures are useful for many reasons, but one of the main advantages is that they allow us to write more concise and expressive code. Closures can be used as arguments to higher-order functions, which are functions that take other functions as parameters or return them as results. Higher-order functions enable us to write generic and reusable code that can operate on different types of data and behavior. For example, the standard library provides many higher-order functions for working with iterators, such as map, filter, fold, and find. These functions take a closure as an argument and apply it to each element of the iterator, producing a new iterator or a final value. For example, this code uses the map function to create a new iterator that contains the squares of the original iterator:

let numbers = vec![1, 2, 3, 4, 5];
let squares = numbers.iter().map(|x| x * x); // map takes a closure as an argument
for square in squares {
println!("{}", square); // prints 1, 4, 9, 16, 25
}

Using closures as arguments to higher-order functions allows us to write more concise and expressive code than using regular functions. For example, this code does the same thing as the previous one, but using a regular function instead of a closure:

let numbers = vec![1, 2, 3, 4, 5];
fn square(x: &i32) -> i32 {
x * x
}
let squares = numbers.iter().map(square); // map takes a function as an argument
for square in squares {
println!("{}", square); // prints 1, 4, 9, 16, 25
}

As you can see, using a regular function requires us to define it separately, give it a name, and specify its input and output types. Using a closure, on the other hand, allows us to define the function inline, without a name, and with inferred types. This makes the code more concise and expressive, as well as more flexible and adaptable to different situations.

Another advantage of closures is that they can capture variables from their environment, which allows us to customize their behavior based on the context. For example, this code uses a closure to create a custom comparator for sorting a vector of strings by length:

let mut words = vec!["hello", "world", "rust", "is", "awesome"];
words.sort_by(|a, b| a.len().cmp(&b.len())); // sort_by takes a closure as an argument
println!("{:?}", words); // prints ["is", "rust", "hello", "world", "awesome"]

The closure captures the len method from the String type and uses it to compare the strings by their length. This way, we can sort the vector by a custom criterion without having to define a separate function or a new type. We can also easily change the closure to sort by a different criterion, such as alphabetical order or reverse order, by modifying the closure body.

Conclusion

Closures are anonymous functions that can capture and use variables from their defining environment. They are a powerful and expressive feature of Rust that allow us to write more concise, flexible, and elegant code. Closures can be used as arguments to higher-order functions, which enable us to write generic and reusable code that can operate on different types of data and behavior. Closures can also capture variables from their environment, which allow us to customize their behavior based on the context. Closures are one of the reasons why Rust is a fun and productive language to learn and use.🦀

Support me by buying an item from my wishlist, visiting my reviews site or buy me a coffee!

Learn More

--

--