What are closures in Rust?
What Are Closures?
What Are Closures?
A closure, also known as an anonymous function or lambda expression, is a function defined within the context of its usage. They allow functions to be defined within the scope of another function and can capture variables from their surrounding scope.
In Rust, closures are defined using a pair of vertical bars (||
), which act like the brackets of a function. Between these bars, we specify the arguments to the function, followed by the code block that constitutes the body of the function. Let's look at a basic example:
let add_one = |x| x + 1;
println!("{}", add_one(5)); // Output: 6
Here, add_one
is a closure that takes one argument, x
, and returns x + 1
.
Capturing Variables
A strong feature of closures is their ability to capture variables from their surrounding environment, commonly called upvar capturing. Variables can be captured in three ways:
- By reference:
&T
- By mutable reference:
&mut T
- By value:
T
Rust will try to infer the least restrictive choice based on how you use the variables within the closure. Let's demonstrate this:
let x = 7;
let borrow = || println!("borrow: {}", x);
let borrow_mut = || println!("borrow_mut: {}", x);
let move = move || println!("move: {}", x);
borrow();
borrow_mut();
move();
In this example, borrow
captures x
by reference (&T
), borrow_mut
captures x
by mutable reference (&mut T
), and move
captures x
by value (T
). The move
keyword is used to force the closure to take ownership of the values it's using.
Type Inference and Genericity
Rust's closure implementation shines when it comes to type inference and genericity. While function definitions require explicit types for their arguments, Rust closures do not. Additionally, unlike functions, closures can be generic over their input types. Here's an example:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
In this code, example_closure
can take either a String
or an i32
because it is generic.
Fn, FnMut, and FnOnce
In Rust, we have three traits to represent three kinds of closures: Fn
, FnMut
, and FnOnce
, which correspond to the three ways of capturing variables:
Fn
takes variables by reference (&T
)FnMut
takes variables by mutable reference (&mut T
)FnOnce
takes variables by value (T
)
Each closure implements one or more of these traits. If a closure implements Fn
, then it also implements FnMut
and FnOnce
. If it implements FnMut
, it also implements FnOnce
.
let x = 7;
let fn_closure = || println!("{}", x); // Implements Fn
let mut fnmut_closure = || println!("{}", x); // Implements FnMut
let fnonce_closure = move || println!("{}", x); // Implements FnOnce
Practical Uses of Closures
There are many ways to use closures in Rust to write concise and flexible code. Let's discuss some of them:
As Arguments to Functions
Functions in Rust can take closures as arguments. This is often used in higher-order functions, which take other functions as arguments or return them as results.
Here's an example where we use a closure to implement a map
function, similar to the map
function on iterators:
fn map<F>(value: i32, func: F) -> i32
where
F: Fn(i32) -> i32,
{
func(value)
}
let double = |x| x * 2;
println!("{}", map(5, double)); // Output: 10
In this example, map
takes an i32
and a closure as arguments applies the closure to the value and returns the result.
Closures in Data Structures
Storing a closure in a struct for later execution is a typical pattern in Rust, particularly in event-driven or callback-driven designs.
struct Button<F> {
callback: F,
}
impl<F: Fn()> Button<F> {
fn new(callback: F) -> Self {
Self { callback }
}
fn click(&self) {
(self.callback)();
}
}
let button = Button::new(|| println!("Button clicked!"));
button.click(); // Output: Button clicked!
In this example, we create a Button
struct that stores a closure as a callback. When the button is clicked, the callback is invoked.
Closures with Iterators
Closures are used extensively with iterators in Rust. They provide a concise way to perform operations on each collection element. For example:
let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<_> = numbers.iter().map(|x| x * x).collect();
println!("{:?}", squares); // Output: [1, 4, 9, 16, 25]
In this example, the closure |x| x * x
is passed to the map
method of the iterator, which applies the closure to each element.
Check out more articles about Rust in my Rust Programming Library!
Conclusion
Rust's closures are an exciting feature allowing flexibility and conciseness in your code. They capture their environment flexibly, can be used similarly to functions, and bring advantages like catching scope and type inference. They can be used as function parameters, stored in data structures for later use, and paired with iterators for efficient data manipulation.
Stay tuned, and happy coding!
Check out more articles about Rust in my Rust Programming Library!
Visit my Blog for more articles, news, and software engineering stuff!
Follow me on Medium, LinkedIn, and Twitter.
Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.
All the best,
Luis Soares
CTO | Head of Engineering | Blockchain Engineer | Solidity | Rust | Smart Contracts | Web3 | Cyber Security
#blockchain #rust #programming #language #traits #abstract #polymorphism #smartcontracts #network #datastructures #data #smartcontracts #web3 #security #privacy #confidentiality #cryptography #softwareengineering #softwaredevelopment #coding #software