
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:
<functional> <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.
Introducing the Closure
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:
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.
Borrowing Immutably
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:
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❯ ./immutableValue of vector: [1, 2, 3]
Borrowing Mutably
If we want the closure to be able to modify the variables that it captures, we can perform a mutable borrow.
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❯ ./mutableValue 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:
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.rserror[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 here6 |7 | println!("Value of vector: {:?}", vector); | ^^^^^^ immutable borrow occurs here8 |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 errorFor more information about this error, try `rustc --explain E0502`.
Taking Ownership
Closures are also capable of taking ownership of a value that they capture. Klabnik (2023) uses threading as an example for when ownership would be useful.1 When spawning a thread with a closure, if the closure captures values from the scope it is defined in, the closure must take ownership of those values given the possibility that the closure could outlive the original values. Let’s look at how a closure would take ownership:
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❯ ./ownershipValue 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.rserror[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 errorFor 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.
When to Use
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?
Threading
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.
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_threadsValue: 1Value: 2Value: 3Value: 4Value: 5Value: 6Value: 7Value: 8Value: 9Value: 10
Iterators and Filters
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.
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❯ ./iteratorsPrevious 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.
Callbacks
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:
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❯ ./callbacksExecuting callback
Conclusion
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.
References
- Klabnik, S., & Nichols, C. (2023). The Rust Programming Language, 2nd Edition. (p. 279). No Starch Press. ↩︎