Finding Closure: Rust’s Anonymous Functions

In software development, there exists the concept of functions. A function is generally defined by a signature, and that signature provides some defining information about the function, i.e., name, parameters, and return type. Functions are blocks of code that can be reused throughout a program through invocation. Simply call a function using its name, and provide the function with any arguments that it requires. Functions are fundamental and essential concepts that all developers should understand and know how to use correctly.

Some languages have special types of functions called anonymous functions. These are functions that do not have names, and they are defined using different syntax from named functions. Anonymous functions in C++ are known as lambda expressions, and they are generally implemented as such:

lambda.cpp
C++
#include <functional>
#include <iostream>
void evaluator(std::function<void()> func) {
func();
}
int main() {
auto lambda = []() {
std::cout << "Lambda was executed" << std::endl;
};
evaluator(lambda);
return 0;
}

We create the lambda expression, assign it to a variable, and then we can pass the variable to another function that is able to accept it as an argument. This is the general format and use case for anonymous functions in many programming languages; however, the topic of this post is specifically targeting anonymous functions in Rust. Let’s get right into it.

Rust has its own interpretation of anonymous functions called closures. Like lambda expressions, closures can be assigned to variables and can be passed to other functions to be evaluated. Like functions, closures can take arguments that can be operated upon within their body, but unlike functions, closures can “capture” the values of other variables within scope. Let’s take a look at the closure’s syntax and break down its essential parts:

closure.rs
Rust
let closure = |param: u32| -> u32 {
let val = param * 2;
val
};

The pipe (|) characters contain between them any parameters that the closure requires. In the above example, the closure takes a parameter param of type u32. If a closure does not have any parameters, the pipes would be “empty”, i.e., ||. Optionally, closures may have return types, and in the above example, this is marked syntactically as -> u32, demonstrating that the closure will return a 32-bit unsigned integer. The body of the closure is wrapped in curly braces ({}), and, when the closure is invoked, the body is evaluated as defined.

The example above is interesting on its own, but it does not demonstrate the true power of the closure. As mentioned previously, closures can capture the values of variables defined in the same scope, something of which standard functions are incapable. Closures are able to capture values in three ways, so let’s take a brief look at all three.

If we just need the closure to capture a value for read-only purposes, we can perform an immutable borrow. This ensures that the closure cannot mutate the value of what is being captured. Here is such an example:

immutable.rs
Rust
fn main() {
let vector = vec![1, 2, 3];
let immutable_borrow = || println!("Value of vector: {:?}", vector);
immutable_borrow();
}

Notice that the closure does not have any parameters between the pipes, so it is not taking vector as an argument; rather, the body of the closure is capturing vector from the current scope, and it is able to read from vector and print its contents. To confirm, we compile and run the code:

❯ rustc immutable.rs
❯ ./immutable
Value of vector: [1, 2, 3]

If we want the closure to be able to modify the variables that it captures, we can perform a mutable borrow.

mutable.rs
Rust
fn main() {
let mut vector = vec![1, 2, 3];
let mut mutable_borrow = || vector.push(4);
mutable_borrow();
println!("Value of vector: {:?}", vector);
}

Notice that both the variable being captured and the variable storing the closure are declared using the mut keyword. The closure captures vector and mutates it by pushing a new value to it. Compiling and running confirms this:

❯ rustc mutable.rs
❯ ./mutable
Value of vector: [1, 2, 3, 4]

Something to note, however, is that all of Rust’s borrow-checker rules still apply. Since vector is being borrowed mutably, immutable borrows may not occur while the closure borrows it. For example, the code below does not compile:

incorrect_borrow.rs
Rust
fn main() {
let mut vector = vec![1, 2, 3];
let mut mutable_borrow = || vector.push(4);
// This is an immutable borrow, which does not compile!
println!("Value of vector: {:?}", vector);
// The mutable borrow is not resolved until this invocation
mutable_borrow();
println!("Value of vector: {:?}", vector);
}

For completeness, this is what the compiler outputs when attempting to compile the above code:

❯ rustc incorrect_borrow.rs
error[E0502]: cannot borrow `vector` as immutable because it is also borrowed as mutable
--> incorrect_borrow.rs:7:39
|
5 | let mut mutable_borrow = || vector.push(4);
| -- ------ first borrow occurs due to use of `vector` in closure
| |
| mutable borrow occurs here
6 |
7 | println!("Value of vector: {:?}", vector);
| ^^^^^^ immutable borrow occurs here
8 |
9 | mutable_borrow();
| -------------- mutable borrow later used here
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.
ownership.rs
Rust
use std::thread;
fn main() {
let vector = vec![1, 2, 3];
thread::spawn(move || println!("Value of vector: {:?}", vector))
.join()
.unwrap();
}

Notice the introduction of the move keyword before the closure is defined. Before discussing why this keyword is needed, let’s first compile this code to make sure it works:

❯ rustc ownership.rs
❯ ./ownership
Value of vector: [1, 2, 3]

Nice, the thread is spawned with the closure, and the closure captures the value of vector which is then printed to the console. So, why the move keyword? When threading, it is possible that the main thread finishes before the spawned thread. If we do not force the closure to acquire ownership of the captured value, we’d instead have an immutable reference, and that reference would become invalid once it falls out of scope (i.e., when the main thread finishes execution). So, to prevent this from occurring, we must make the closure take ownership of the captured value. In fact, the compiler forces us to use the move keyword. This is the compiler output if we elect not to use it:

❯ rustc ownership.rs
error[E0373]: closure may outlive the current function, but it borrows `vector`, which is owned by the current function
--> ownership.rs:8:19
|
8 | thread::spawn(|| println!("Value of vector: {:?}", vector))
| ^^ ------ `vector` is borrowed here
| |
| may outlive borrowed value `vector`
|
note: function requires argument type to outlive `'static`
--> ownership.rs:8:5
|
8 | thread::spawn(|| println!("Value of vector: {:?}", vector))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `vector` (and any other referenced variables), use the `move` keyword
|
8 | thread::spawn(move || println!("Value of vector: {:?}", vector))
| ++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0373`.

By “moving” the captured value into the closure, the closure takes ownership of the value so that it does not have to rely on the validity of a reference.

Now that we know the various ways that closures can capture values from their environment, when and where should we use closures as opposed to normal functions?

One such use case that we already demonstrated is with threading. When a thread is spawned, it needs work to do. The easiest and most idiomatic way of providing a thread with work is to use a closure. The thread::spawn method accepts an argument of type FnOnce(); closures that use the move keyword are typically of this type.

more_threads.rs
Rust
use std::thread;
fn main() {
let big_list = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let thread = thread::spawn(move || {
for x in big_list {
println!("Value: {}", x);
}
});
thread.join().unwrap();
}

We pass a closure to the thread, and we capture the needed values with ownership so that the thread can safely access the data. Let’s attempt to compile and run the code:

❯ rustc more_threads.rs
❯ ./more_threads
Value: 1
Value: 2
Value: 3
Value: 4
Value: 5
Value: 6
Value: 7
Value: 8
Value: 9
Value: 10

Another common use case is iterators. In Rust, iterators have several utility methods for performing operations while iterating or for filtering data. One such utility method is map() which accepts a closure that would allow for some set of operations to be run with or applied to the values within the collection being iterated over.

iterators.rs
Rust
fn main() {
let list = vec![1, 2, 3];
println!("Previous list: {:?}", list);
let new_list: Vec<_> = list.iter().map(|x| x * x).collect();
println!("New list: {:?}", new_list);
}

In the code above, we declare an immutable list, we print the values, and then we generate a new list (assigned to new_list) using iter() to iterate over the original list and then using map() to specify a closure that will modify the values from the original list before storing them in the new list with collect(). Compiling and running the above code produces this output:

❯ rustc iterators.rs
❯ ./iterators
Previous list: [1, 2, 3]
New list: [1, 4, 9]

In our closure, we take in a parameter x, and we implicitly return the result of x * x. We can verify the closure works in this manner due to the output of the new list as seen above.

One other use, though certainly not the last, is using closures for callbacks. Callbacks are heavily used in event-based programming. A callback is some function or bit of code that is executed in response to some event trigger. Closures make defining callbacks idiomatic and simple. Here is an example:

callbacks.rs
Rust
fn do_something<F: Fn()>(callback: F) {
callback();
}
fn main() {
do_something(|| println!("Executing callback"));
}

In the above code, the function do_something() takes an argument of type Fn(), i.e., a function, namely a closure. We create a simple closure that just prints a string to the console. The closure is invoked in the body of the do_something() function. Let’s see it in action:

❯ rustc callbacks.rs
❯ ./callbacks
Executing callback

This article just covers some surface-level information about Rust’s closures. They can be a very powerful tool when used correctly, and knowing how to appease the borrow-checker is crucial to success. If you are interested in learning about some advanced closure techniques, or if you just want to stay informed on the latest posts, be sure to subscribe using the subscription box below.

  1. Klabnik, S., & Nichols, C. (2023). The Rust Programming Language, 2nd Edition. (p. 279). No Starch Press. ↩︎

Discover more from shared_ptr

Subscribe now to keep reading and get access to the full archive.

Continue reading