Rust Closures
Intro
I'm new to Rust and programming in general, and while I've dabbled in different languages and technologies, I've never gone headfirst into any language to become proficient. I'm trying to change that now with Rust. Please forgive my inaccuracies or lack of understanding of more complex topics, but I'll get there eventually. For now, I thought closures were an interesting topic, so I decided to write about this this topic first.
What are closures?
A closure can be thought of as a quick function that doesn't need a name, or an anonymous function that can be saved in a variable or passed into other functions as arguments. A unique thing about closures is that they can capture their environment and this makes them a versatile tool that can be fun and efficient to use.
Closures are easy to identify because they use pipes ||
instead of the typical parentheses ()
that functions use. There are a few different ways you can use closures in Rust, but first, let's look at how a closure is created.
How are closures created in Rust?
Before we look at closures let us take a quick look at the syntax of how a function is defined in Rust. Below is a simple function definition that adds 1 to its parameter.
// function definition syntax
fn plus_one_v1 (x: u32) -> u32 { x + 1 }
If you're familiar with Rust's functions this should be pretty easy to understand, so we won't go through the details, if you're not familiar with creating a function in Rust here's a Let's Get Rusty tutorial on functions that will bring you up to speed. Now let's look at how we'd create a closure using similar syntax to a function.
//closure definition syntax
let plus_one_v2 = | x: u32| -> u32 { x + 1 };
As you can see the syntax is very similar to a function other than we use the let
keyword to bind the closure to plus_one_v2
, and use the equal sign before the pipes ||
. One thing to note is that a lot of this syntax is optional for closure definitions, the Rust compiler is usually able to infer the types in closures. Keep in mind that when the Rust compiler infers the type, it will take the first type we pass through as the type it uses. So you can't change the type later in your code and expect it to work.
Simplifying Syntax
Since the Rust compiler can infer the types, to declare this closure in the most succinct manner you could do so like this:
// closure definition stripped down
let plus_one_v2 = |x| x + 1;
Let's go step by step and see what we removed.
// closure syntax defined
let plus_one_v2 = | x: u32| -> u32 { x + 1 };
// removing the return type and curly braces
let plus_one_v2 = | x: u32| -> x + 1;
// removing the u32 type in closure
let plus_one_v2 = |x| x + 1;
Again because the rust compiler can infer the type we can make this line of code much cleaner.
Just like in functions where the parentheses ()
can be empty, we can leave the ||
empty or add variables and types. Let's look at another example.
// empty `||`
fn main() {
let simple_closure = || println!("A simple closure");
simple_closure();
}
Here we define a closure to the variable simple_closure
and this has nothing in between ||
. If we run this code it prints:
A simple closure.
Passing in Parameters
Now let's look at what happens when we add something in between the pipes ||
similar to how we would to the parenthesis ()
of a function. We define x
as and i32
which means that when we call simple_closure
we can pass in an i32
into it.
// pipes with an i32
fn main() {
let simple_closure = |x: i32| println!("{}", x);
simple_closure(3);
simple_closure(3+16);
}
This prints:
3
19
So far so good, I hope. This behavior here is just like a function, we're not doing anything too different.
Capturing the Environment
Now let's look at what makes closures special, "capture their environment", meaning they can take variables that are outside of the closure.
// Capturing the environment
fn main() {
let first = 12
let second = 3
let capture_closure = || println!("{}", first + second);
capture_closure();
}
This prints:
15
So what is going on here? We didn't put anything in between the ||
but closures can just 'take' the variables first
and second
and add them because closures can capture their environment -- pretty cool huh?
The 3 closure traits (Fn, FnMut, and FnOnce)
Now, let's dive a little deeper and take a look at the Fn
, FnMut
, and FnOnce
traits and how they work with closures. This happens behind the scenes when using closures with variables but in order to understand closures and be able to use them to their full potential we must understand what is happening with these 3 traits.
Fn
: using this with an upper caseF
means we are not mutating the captured variables and they are read-only.FnMut
: allows us to change captured variables, creating a mutable borrow of these captured variables.FnOnce
: this lets us move ownership into the closure. In essence, consuming the variable.
Again, Rust infers these traits when using closures with variables, different than when they're being used with functions where they have to be explicitly defined, but we'll look at that later.
Fn
Trait
Let's take a quick look at what each of these looks like in code.
// using `Fn` trait
fn main() {
let str1 = "Hello".to_string();
let closure = |x| println!("{} {}", str1, x);
closure("rustaceans!")
}
Here we capturing the variable and printing it with the Fn
trait because we are immutably borrowing that variable and printing it to screen. which prints:
Hello rustaceans!
As you can tell we didn't actually have to define the Fn
trait it was implied by how it the code is constructed.
FnMut
Trait
Here in the 2nd block of code with clsr2
we are declaring a closure that actually mutates the str1
variable, so here we are using the FnMut
trait because we are mutating our variable.
fn main() {
// using Fn trait
let mut str1 = "Hello".to_string();
let closure = |x| println!("{} {}", str1, x);
closure("rustaceans!");
// using FnMut trait
let mut closure2 = |x| str1.push_str(x);
closure2(" welcome back, rustaceans!");
println!("{}", str1);
}
This now prints:
Hello rustaceans!
Hello welcome back, rustaceans!
FnOnce
Trait
In this last example, we take a look at the FnOnce
trait
fn main() {
// using Fn trait
let mut str1 = "Hello".to_string();
let closure = |x| println!("{} {}", str1, x);
closure("rustaceans!");
// using FnMut trait
let mut closure2 = |x| str1.push_str(x);
closure2(" welcome back, rustaceans!");
println!("{}", str1);
// using FnOnce trait
let closure3 = || drop(str1);
println!("before dropping str1");
closure3();
println!("str1 has been dropped");
}
We see the FnOnce
trait in action here when using drop(str1)
. However, if we try to call str1
again, our code would not compile and give us an error message, so we don't call it in this instance.
The move
keyword
The move
keyword comes in handy when you want to force a closure to take ownership of the values it captures, even though the closure doesn't need it. move
can specifically come into action when passing a closure into a new thread and thus moving the data so it's owned by this new thread. Threads are something we can cover at a different time, but let's look at this example on how to use the move
keyword with closures:
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}
A new thread is spawned and we force the closure to take ownership of the list
by using the move
keyword. In this way even if we implement the Fn
or FnMut
traits, ownership is moved to the closure allowing more flexibility in the closure.
Passing closures(or other functions) as inputs to functions
Now let's look at a couple of different ways we can use closures as inputs to functions, building on what we saw above with the Fn
, FnMut
.
As mentioned outside of a function, a closure can decide by itself which trait to use, but inside of a function, you have to be explicit and define one of these traits.
// using `Fn`
fn add_num<F>(func: F)
where F: Fn(i32){
func(7)
}
fn main(){
let num = 6;
add_num(|x|println!("{}", num + x));
}
This prints:
13
Here we are defining the Fn
trait to add two numbers and since we are not changing the number in the add_num
function and simply passing it through we can define the Fn
trait without major fuss.
However, if we try to make our num
mutable by incrementing it, we might run into some problems. Let's see what happens.
fn add_num<F>(func: F)
where F: Fn(i32){
func(7)
}
fn main(){
let mut num = 6; // mutable to allow incrementation
add_num(|x|{num +=x; println!("{}", num + x)});
}
As expected we get an error message:
error[E0594]: cannot assign to `num`, as it is a captured variable in a `Fn` closure
--> src/main.rs:8:14
|
1 | fn add_num<F>(func: F)
| - change this to accept `FnMut` instead of `Fn`
...
8 | add_num(|x|{num +=x; println!("{}", num + x)});
| ------- --- ^^^^^^^ cannot assign
| | |
| | in this closure
| expects `Fn` instead of `FnMut`
The compiler actually tells us what we should be doing here (very nice), changing the Fn
to FnMut
.
If we update our code to in the add_num
function to what is shown below, our problems will be solved:
fn add_num<F>(mut func: F) // add mut here
where F: FnMut(i32){ // change Fn to FnMut here
func(7)
}
fn main(){
let mut num = 6;
add_num(|x|{num +=x; println!("{}", num + x)});
}
This now compiles and prints:
20
Conclusion
Phew, we covered a lot but there's still more we can do with closures, like storing them in structs and using them in conjunction with iterators, but we'll leave that for another time. For now, I hope I have been able to explain what a closure is, how to create a closure, and how to use its traits. This should help you get started with getting familiar with closures and understanding how to use them in your own code.
Please let me know if I got something wrong or was unclear on anything, I'm doing this to help me better learn these concepts, so any feedback is appreciated.