Closures: Magic Functions
If you want to respond to this post, please respond via Rust users or Reddit.
Closures seem like magical functions. They can do magic like capture their environment, which normal functions can’t do.
let hello_world = "Hello World!".to_string();
let closure = || println!("{}", hello_world);
// error[E0434]: can't capture dynamic environment in a fn item
// fn normal_function() {
// println!("{}", hello_world)
// }
How does this work?
The first thing to understand about closures is that they are pure sugar, and three traits working in concert.
Fn* Traits
The three traits are
trait FnOnce<Args> { type Output; fn call_once(self, args: Args) -> Self::Output; }
trait FnMut<Args> : FnOnce<Args> { fn call_mut(&mut self, args: Args) -> Self::Output; }
trait Fn<Args> : FnMut<Args> { fn call(&self, args: Args) -> Self::Output; }
note I have removed some unnecessary details, like function calling convention to simplify. You can see the docs for more info about each one here: FnOnce, FnMut, Fn.
These traits are critical to how closures work, so let’s delve into how they work.
Args
First the type parameter: Args
Args
must always be a tuple representing the arguments of the closure.
for example
|| "hi"; // this has Args = ()
|a: u32| (); // this has Args = (u32,)
|a: f32, q: String| a; // this has Args = (f32, String)
This is to get around needing variadic generics to handle every possible list of arguments.
This representation is unstable, and may change in the future. So instead of using the Fn*
traits
directly, you can use them like so
Fn() -> u32
FnMut(u32, u32)
FnOnce(String) -> Vec<u8>
Output
Next is type associated type Output
.
This is quite simple, it just represents the output type of the closure.
|| "hi"; // this has Output = &'static str
|a: u32| (); // this has Output = ()
|a: f32, q: String| a; // this has Output = f32
call*
Finally is the functions
fn call*(self, args: Args) -> Self::Output;
These functions do the leg work of executing the closure. There are a few notable differences between each one.
- In
FnOnce
we havecall_once
, which takes aself
receiver. This is how it enforces that it is only called once. After self is moved into this function call, it can’t be used again. - In
FnMut
we havecall_mut
, which takes a&mut self
receiver. This allows changes to the environment in closures. - In
Fn
we havecall
, which takes a&self
receiver. This doesn’t allow changes to the environment (ignoring shared mutability), but it does make it the most flexible type of closure. It can be called as many times as you want, and it can be thread-safe ifSend
andSync
are also implemented for the closure.
Examples
I believe that working by example is the best way to explain something.
I will show the desugaring of a few closures, and explain why they are that way, and some benefits and costs to each closure.
Note: I will not show how Send
and Sync
are impled, as that is out of scope. After the first desugaring, I will not show the impls for all three Fn*
traits, only the most specific one. So if you see Fn
, then assume FnMut
and FnOnce
are impled with the same function body. If you see FnMut
, then assume that FnOnce
is impled with the same function body, but Fn
is not impled. If you see FnOnce
, then assume that Fn
and FnMut
are not impled. I will also put type Output
in a comment to show what it would be if I only impl Fn
or FnMut
.
First, the simplest closure, one that doesn’t capture anything, and only returns a unit.
let x = || ();
let y = x();
gets desugared to
#[derive(Clone, Copy)]
struct __closure_0__ {}
impl FnOnce<()> for __closure_0__ {
type Output = ();
fn call_once(self, args: ()) -> () {
()
}
}
impl FnMut<()> for __closure_0__ {
fn call_mut(&mut self, args: ()) -> () {
()
}
}
impl Fn<()> for __closure_0__ {
fn call(&self, (): ()) -> () {
()
}
}
let x = __closure_0__ {};
let y = Fn::call(&x, ());
You can use these playground links to test out the desugared code!
Now, there is quite a bit to unpack here. First we get this new type __closure_0__
. We can also see that Clone
and Copy
are derived for __closure_0__
. This is because it is an empty type so it is trivial to Clone
and Copy
an empty struct. This allows for more flexibility when using the closure.
Rust will pick the most specific Fn*
trait to use whenever you call a function, in this order: Fn
, FnMut
, FnOnce
. So in this case, because we can implement Fn
, we implement that and all pre-requisites (FnMut
and FnOnce
). The function body from the closure is copied over to the function body of each of call*
functions.
Then create the closure by creating this new struct. We call the closure by calling the most specific call*
function, which in this case is call
.
Note: the names I give, __closure_0__
are arbitrary and the names that are actually used are random. This makes closures unnameable.
Note: How Rust knows which Fn*
trait to derive for the closure is up to analysis of what it captures and how it is used (seen later).
Now one step up, lets capture a variable.
let message = "Hello World!".to_string();
let print_me = || println!("{}", message);
print_me();
desugars to
#[derive(Clone, Copy)]
struct __closure_1__<'a> { // note: lifetime parameter
message: &'a String, // note: &String, not String
}
impl<'a> Fn<()> for __closure_1__<'a> {
// type Output = ();
fn call(&self, (): ()) -> () {
println!("{}", *self.message)
}
}
let message = "Hello World!".to_string();
let print_me = __closure_1__ { message: &message };
Fn::call(&print_me, ());
We now have a field on __closure_1__
, this represents the environment that is being used. So when we go to implement the Fn*
traits, we use these fields to get access to the environment. Whenever Rust accesses one of these fields, it first dereferences them, the reason why will become evident when we get to mutating closures.
Notice the lifetime parameter on __closure_1__
, because it is borrowing from the stack frame with &message
, print_me
has a non-'static
lifetime. One downside to this is that it can’t be sent across threads! Threads require a 'static
lifetime so that things don’t deallocate while they run.
We still maintain Clone
and Copy
because shared references are Copy
.
Now, what about if I have a closure with arguments? What about move
closures?
let header = "PrintMe: ".to_string();
let print_me = move |message| println!("{}{}", header, message);
print_me("Hello World!");
desugars to
#[derive(Clone)]
struct __closure_2__ { // note: no lifetime parameter
header: String // note: String, not &String
}
impl<'a> Fn<(&'a str,)> for __closure_2__ {
// type Output = ();
fn call(&self, (message,): (&'a str,)) -> () {
println!("{}{}", self.header, message);
}
}
let header = "PrintMe: ".to_string();
let print_me = __closure_2__ {
header: header // note: no &
};
Fn::call(&print_me, ("Hello World!",));
First, the types of the closure arguments are resolved via type inference.
Next, how are arguments handled? As we saw earlier in the Fn* Traits
section, arguments are really just a single tuple containing all of the arguments. This tuple is automatically created whenever we call a closure and destructured inside the call*
function.
Finally, what did move
do? Simply, instead of borrowing from the environment, we are going to move everything from the environment into this new anonymous struct (__closure_2__
). Now because __closure_2__
doesn’t contain any lifetimes, it has a 'static
lifetime, which is necessary for it to be sent across threads! But in doing so, we also lost Copy
, now our closure in only Clone
. :(
This is why when you do anything with threads, you need to use move
closures. They eliminate many of the references that would otherwise be created.
More on move
let a = "Hello World".to_string();
let a_ref = &a;
let print_me = move || println!("{}", a_ref);
print_me();
desugars to
// lifetimes are back, even though this is a `move` closure
// because this closure captures a reference
// note: a new lifetime parameter will be created for
// user-defined structs that also have lifetime parameters.
#[derive(Clone, Copy)]
struct __closure_3__<'a> {
a_ref: &'a String
}
impl<'a> Fn<()> for __closure_3__<'a> {
// type Output = ();
fn call(&self, (): ()) {
println!("{}", self.a_ref)
}
}
let a = "Hello World".to_string();
let a_ref = &a;
// because this is a move closure, there are no new references here
let print_me = __closure_3__ { a_ref: a_ref };
Fn::call(&print_me, ());
Notice that even though we have a move
closure, we still get lifetimes. This is because we have a reference from the environment. This means that unless that reference resolves to be 'static
, you cannot send it across threads. In this case the reference is definitely a shorter lifetime than 'static
What about returning things from closures, and mutating the environment inside a closure.
let mut counter: u32 = 0;
let delta: u32 = 2;
let mut next = || {
counter += delta;
counter
};
assert_eq!(next(), 2);
assert_eq!(next(), 4);
assert_eq!(next(), 6);
desugars to
struct __closure_4__<'a, 'b> {
counter: &'a mut u32,
delta: &'b u32
}
impl<'a, 'b> FnMut<()> for __closure_4__<'a, 'b> {
// type Output = u32;
fn call_mut(&mut self, (): ()) -> u32 {
*self.counter += *self.delta;
*self.counter
}
}
let mut counter: u32 = 0;
let delta: u32 = 2;
let mut next = __closure_4__ {
counter: &mut counter,
delta: &delta
};
assert_eq!(FnMut::call_mut(&mut next, ()), 2);
assert_eq!(FnMut::call_mut(&mut next, ()), 4);
assert_eq!(FnMut::call_mut(&mut next, ()), 6);
Because we are changing counter inside of the closure, we can implement at most FnMut
. This is because we don’t have write access inside of Fn
.
We take a &mut
to counter
so that we can change it, and a &
to delta
to read from it. Each reference gets a fresh lifetime parameter.
We can now see why we need to dereference the references inside of call*
. This is because we need the correct types for things to work out. For example, there is no impl of AddAssign
for &mut u32
, but there is one for u32
. So we need to dereference self.counter
so that Rust can resolve AddAssign
correctly. There is nothing special about AddAssign
, the type checker requires that these types are dereferenced in order to work correctly.
What about consuming things in a closure
let a = vec![0, 1, 2, 3, 4, 5, 100];
// notice, no `move`
let transform = || {
let a = a.into_iter().map(|x| x * 3 + 1);
a.sum::<u32>()
};
println!("{}", transform());
// println!("{}", transform()); // error[E0382]: use of moved value: `transform`
desugars to
#[derive(Clone)]
struct __closure_5__ {
a: Vec<u32>
}
impl FnOnce<()> for __closure_5__ {
type Output = u32;
fn call_once(self, (): ()) -> u32 {
let a = self.a.into_iter().map(|x| x * 3 + 1);
a.sum::<u32>()
}
}
let a = vec![0, 1, 2, 3, 4, 5, 100];
let transform = __closure_5__ { a: a };
println!("{}", FnOnce::call_once(transform, ()));
// println!("{}", transform.call_once(())); // error[E0382]: use of moved value: `transform`
Even though we didn’t add the move
qualifier to the closure we see that a
was moved into the closure. This is because Vec::into_iter
takes self
be value, which means self
will be moved into the function. Because of this, the Rust moves a into __closure_5__
. This means that a
must be consumed during the function call, only FnOnce
can be implemented.