Unlocking the Power of Closures in Rust: Closures in rust are different and powerful

Chidozie C. Okafor
4 min readApr 10, 2023

--

Closures are a powerful Rust feature that allows functions to capture their surroundings. Understanding closures as a professional developer is critical for writing efficient, modular, and concise code. In this article, we’ll look at the concept of closures and how they can capture variables, interact with input and output parameters, and more. We’ll use illustrations and multiple examples to provide a thorough understanding of Rust closures. Let’s get started on our journey through the world of closures!

Closures: The Environment-Capturing Functions

Closures are functions that can capture their surroundings and use variables from outside their scope. The |…| syntax is sometimes used to define a closure.

Consider closures to be a camera that can capture a snapshot of their surroundings. They can “remember” variable values at the time of the snapshot and use them later when called.

let x = 3;
let closure = |num| num * x;
let result = closure(2);

The closure in this example captures the variable `x` from its environment and multiplies it by the input `num`.

Capturing: The Flexible Nature of Closures

Closures can capture variables in three ways, depending on the functionality needed:

  1. By reference: &T
  2. By mutable reference: &mut T
  3. By value: T

Imagine closures capturing variables as different types of containers:

  1. By reference: A transparent container that allows you to see and use the variable while not allowing you to modify or move it.
  2. By mutable reference: A container that allows the variable to be used, modified, and rearranged but not removed.
  3. By value: A container that allows you to extract the variable and use it as you see fit, including moving it to another location.
let x = 3;
let by_ref = || println!("{}", x); // Captures x by reference
let mut y = 5;
let by_mut_ref = || { y += 1; println!("{}", y); }; // Captures y by mutable reference
let by_value = move || println!("{}", x); // Captures x by value

Closures as Input Parameters: The Art of Annotating Traits

When a closure is used as an input parameter, its complete type must be annotated with one of the following traits:

1. Fn: The closure refers to the captured value. (&T)
2. FnMut: The closure makes use of the captured value via the mutable reference. (&mut T)
3. FnOnce: The closure makes use of the captured value by value. (T)

Consider these characteristics to be different types of access levels granted to a visitor to your home:

  1. Fn: The visitor may inspect your belongings but may not touch or modify them.
  2. FnMut: The guest can touch and change your belongings, but they cannot be taken away.
  3. FnOnce: The guest has the option of taking your belongings with them when they leave.
fn apply<F: Fn(i32) -> i32>(f: F, num: i32) -> i32 {
f(num)
}

let double = |x| x * 2;
let result = apply(double, 4);

Type Anonymity: Generics in the World of Closures

Closures have anonymous types that must be used with generics when used as function parameters.

Consider closures to be people dressed in masks at a masquerade ball. You don’t know who they are, but you can still interact with them based on their behavior and characteristics.

fn call_twice<F>(closure: F, value: i32) -> i32
where
F: Fn(i32) -> i32,
{
closure(value) + closure(value)
}

let add_five = |x| x + 5;
let result = call_twice(add_five, 10);

We use generics with the `where` clause in this example to constrain the closure type based on its behavior, allowing us to interact with it without knowing its exact type.

Input Functions: Passing Functions as Parameters

Closures and functions can both be used as arguments. Any function that satisfies the trait bound of that closure can be passed as a parameter when declaring a function that takes a closure as a parameter.

Take a puzzle with a variety of shaped pieces. Closures are irregularly shaped pieces, whereas functions are regular shapes such as squares or circles. If a puzzle slot accepts a unique shape, it may also accept a standard shape that fits within the boundaries of the unique shape.

fn square(x: i32) -> i32 {
x * x
}

let result = apply(square, 4); // Passing a function instead of a closure

Closures as Output Parameters: Returning the Unknown

It is possible to return closures as output parameters, but because anonymous closure types are unknown by definition, we must use impl Trait to do so.

Let’s assume closures to be wrapped presents. You’re giving someone a gift without revealing what’s inside when you return a closure from a function. The impl Trait syntax is similar to wrapping paper in that it conceals the contents.

The following are valid traits for returning a closure:

  1. Fn
  2. FnMut
  3. FnOnce
fn create_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}

let add_five = create_adder(5);
let result = add_five(10);

The create_adder function in this example returns a closure that adds x to its input. The closure captures x by value with the `move` keyword, and the function returns a closure with impl Fn(i32) -> i32.

Closures are a powerful and adaptable feature in Rust that allow you to capture and use variables from the environment. You’ll be better equipped to write efficient, modular, and concise Rust code if you understand how closures can capture variables, interact with input and output parameters, and work with type anonymity.

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC

No responses yet