Understanding Rust Closures
While reading the Explicit capture clauses blog post, I realized that my understanding of rust closures was very superficial. This article is an attempt at explaining what I learned while reading and experimenting on the subject. It starts from the very basics and then explore more complex topics. Note that each title is a link to a rust playground where you can experiment with the code in the section.Closures basics
You probably already know that a closure in rust is a function written with the following syntax:
let double_closure = |x| x * 2; assert_eq!( 4, double_closure( 2 ));
Written as a regular function it looks like:
fn double_function(x: u32 ) -> u32 { x * 2 } assert_eq!( 4, double_function( 2 ));
Very similar. There is actually a small difference between the two, the double_function parameter and return type are u32. On the other hand, because we did not specify any type in double_closure, thedefault integer type has been picked, namely i32.
We can fix that like this:
let double_typed_closure = |x: u32 | -> u32 { x * 2 }; assert_eq!( 4, double_typed_closure( 2 )); assert_eq!( 4, double_typed_closure( 2 u32 )); // assert_eq!(4, double_typed_closure(2u16)); // This would be an error.
And for a classic example usage of closures, we can use the Option::map method:
assert_eq!(Some( 4 ), Some( 2 ).map(|x| x * 2 )); assert_eq!(Some( 4 ), Some( 2 ).map(double_closure)); // double_closure from above assert_eq!(Some( 4 ), Some( 2 ).map(double_function)); // Passing double_function works too!
So, it seems closures are just a shorter syntax for functions with type inference.Capture
The main difference between closures and functions is that closures can capture variables from their environment while functions can't:
let hello = "Hello "; let greeter_closure = |x| String::new() + hello + x;
assert_eq!( "Hello world", greeter_closure( "world" )); assert_eq!( Some( "Hello world".to_owned()), Some( "world" ).map(greeter_closure) );
Notice how the hello variable is used within the body of the greeter_closure. Let's try that with a function:
let hello = "Hello ";
fn greeter_function(x: &str ) -> String { String::new() + hello + x }
error[E0434]: can't capture dynamic environment in a fn item • -> src/main.rs:7:25 | 7 | String::new() + hello + x | ^^^^^ | = help: use the || {... } closure form instead
This does not work and the compiler helpfully suggest to use a closure instead.Capture by shared reference
In the greeter_closure example above, the hello variable was captured by shared reference because the variable is only read. As shown below, we can still use that variable after the closure declaration and usage:
let hello = "Hello "; let greeter_closure = |x| String::new() + hello + x;
// We can still use the hello variable here assert_eq!( "Hello ", hello);
assert_eq!( "Hello world", greeter_closure( "world" ));
// And here assert_eq!( "Hello ", hello);Capture by mutable reference
It is also possible to capture by mutable reference so that the closure can alter the value of the captured variable. See this naive way to compute the sum of integers from 1 to 10:
let mut total = 0; let add_mut_closure = |x| total += x;
// We can't access total here: // assert_eq!(0, total); // error[E0502]: cannot borrow total as immutable because it is also borrowed as mutable
( 1.. = 10 ).for_each(add_mut_closure);
// But we can access total here, now that add_mut_closure is out of scope. assert_eq!( 55, total);Capture by value
Finally, one can capture by value:
let last_word = "last word: ".to_owned(); let drop_closure = |sigh| { let res = String::new() + & last_word + sigh; drop(last_word); // Forcing the capture by value res };
// We can't access last_word here: // assert_eq!("last word: ".to_owned(), last_word); // error[E0382]: borrow of moved value: last_word
assert_eq!( "last word: sigh!", drop_closure( "sigh!" ));
// We can't access last_word here either // assert_eq!("last word: ".to_owned(), last_word); // error[E0382]: borrow of moved value: last_word
// And we can't call drop_closure again // assert_eq!("last word: sigh!", drop_closure("sigh!")); // error[E0382]: use of moved value: drop_closureFnOnce trait
In the previous example, notice the last error when trying to call drop_closure twice. Here is the full error:
error[E0382]: use of moved value: drop_closure • -> src/main.rs:18:32 | 12 | assert_eq!("last word: sigh!", drop_closure("sigh!")); | --------------------- drop_closure moved due to this call ... 18 | assert_eq!("last word: sigh!", drop_closure("sigh!")); | ^^^^^^^^^^^^ value used here after move | note: closure cannot be invoked more than once because it moves the variable last_word out of its environment • -> src/main.rs:5:10 | 5 | drop(last_word); | ^^^^^^^^^ note: this value implements FnOnce, which causes it to be moved when called • -> src/main.rs:12:32 | 12 | assert_eq!("last word: sigh!", drop_closure("sigh!")); | ^^^^^^^^^^^^
The interesting note is:
note: this value implements FnOnce, which causes it to be moved when called
What is that FnOnce implementation the compiler is talking about?
It is a trait automatically implemented by the compiler which state that the closure can be called at least once.
That trait is a bit special because it cannot be implemented manually in stable rust.
However, if we switch to unstable and enable some features, we can play with it and try to desugar how closures are actually implemented by the compiler.
Let's try to desugar the drop_closure above.
First, make sure to switch to the nightly channel and to enable the following features (for example by putting them at the top of your main.rs ):
#![feature(fn_traits)] #![feature(unboxed_closures)]
Next, we need to define a struct having the captured variables as fields:
struct DropStruct { last_word: String, }
Simple enough, we are capturing only one variable so our struct has one field.
Now the FnOnce implementation:
impl FnOnce<( &str,)> for DropStruct { type Output = String; extern "rust-call" fn call_once(self, (sigh,): ( &str,)) -> Self:: Output { let res = String::new() + & self.last_word + sigh; drop(self.last_word); res } }
That is some weird trait!
Let's go step by step.
impl FnOnce<(&str,)> means that we are implementing a closure which takes one parameter which is a &str.
If the closure took two arguments of type i32 and i64 we would have impl FnOnce<(i32, i64)>. (&str,) is the definition of a tuple of one element. See the reference ontuple types for details.
for DropStruct should not be too surprising.
type Output = String specifies that our closure returns a String.
extern "rust-call" is some magic which I won't explain mostly because I don't know exactly why it is required.
The rest of the implementation should be self explanatory. We just took the content of the closure and replaced last_word by self.last_word.
Let's try it:
let last_word = "last word: ".to_owned(); let drop_struct = DropStruct { last_word };
// We could call call_once: // assert_eq!("last word: sigh!", drop_struct.call_once(("sigh!",)));
// But more simply, we can use the function call syntax: assert_eq!( "last word: sigh!", drop_struct( "sigh!" ));
// And we still can't call it twice // assert_eq!("last word: sigh!", drop_struct("sigh!")); // error[E0382]: use of moved value: drop_structFnMut trait
What about our add_mut_closure from before? We were able to call it multiple times and even mutate the capture variables.
That kind of closure implements the FnMut trait.
Let's try to desugar the following closure which push elements in a vector:
let mut v = vec![]; let push_closure = |x| v.push(x);
( 1.. = 5 ).for_each(push_closure); assert_eq!(vec![ 1, 2, 3, 4, 5 ], v);
First we need to define a struct:
struct PusherStruct< 'a > { v: &'a mut Vec< i32 >, }
Because we are capturing by reference, we need to introduce a lifetime.
Now the FnMut implementation:
impl < 'a >…