- Closures
- CIS 198 Lecture 4
- Closures
- Closure Syntax
- Type Inference
- Closure Environment
- Closure Environment
- Closure Environment
- Move Closures
- Move Closures
- Closure Ownership
- Closure Ownership
- Closure Ownership
- Closure Traits
- Closure Traits
- Closures As Arguments
- Returning Closures
- Returning Closures
- Returning Closures
- Returning Closures
Closures
CIS 198 Lecture 4
Closures
- A closure, anonymous function, or lambda function is a common paradigm in functional languages.
- In Rust, they’re fairly robust, and match up well with the rest of Rust’s ownership model.
1. let square = |x: i32| -> i32 { x * x };
2. println!("{}", square(3));
3. // => 6
???
Inline function definitions which can be bound to variables. The function block is executed when the closure is called.
Closure Syntax
1. let foo_v1 = |x: i32| { x * x };
2. let foo_v2 = |x: i32, y: i32| x * y;
3. let foo_v3 = |x: i32| {
4. // Very Important Arithmetic
5. let y = x * 2;
6. let z = 4 + y;
7. x + y + z
8. };
9. let foo_v4 = |x: i32| if x == 0 { 0 } else { 1 };
- These look pretty similar to function definitions.
- Specify arguments in
||, followed by the return expression. - The return expression can be a series of expressions in
{}.
???
letinstead offn- Arguments in pipes
- Braces are optional
Type Inference
1. let square_v4 = |x: u32| { (x * x) as i32 };
3. let square_v4 = |x| -> i32 { x * x }; // ← unable to infer enough
4. let square_v4 = |x| { x * x }; // ← type information!
- Unlike functions, we don’t need to specify the return type or argument types of a closure.
- In this case, the compiler can’t infer the type of the argument
xfrom the return expressionx * x.
???
Having concrete function types for type inference and self-documentation. For closures, ease of use is more important.
Closure Environment
- Closures close over (contain) their environment.
1. let magic_num = 5;
2. let magic_johnson = 32;
3. let plus_magic = |x: i32| x + magic_num;
- The closure
plus_magicis able to referencemagic_numeven though it’s not passed as an argument. magic_numis in the “environment” of the closure.magic_johnsonis not borrowed!
Closure Environment
- If we try to borrow
magic_numin a conflicting way after the closure is bound, we’ll get an error from the compiler:
1. let mut magic_num = 5;
2. let magic_johnson = 32;
3. let plus_magic = |x: i32| x + magic_num;
5. let more_magic = &mut magic_num; // Err!
6. println!("{}", magic_johnson); // Ok!
1. error: cannot borrow `magic_num` as mutable because it is
2. already borrowed as immutable
4. [...] the immutable borrow prevents subsequent moves or mutable
5. borrows of `magic_num` until the borrow ends
- Why?
plus_magicborrowsmagic_numwhen it closes over it! - However,
magic_johnsonis not used in the closure, and its ownership is not affected.
Closure Environment
- We can fix this kind of problem by making the closure go out of scope:
1. let mut magic_num = 5;
2. {
3. let plus_magic = |x: i32| x + magic_num;
4. } // the borrow of magic_num ends here
6. let more_magic = &mut magic_num; // Ok!
7. println!("magic_num: {}", more_magic);
???
Questions?
Move Closures
- As usual, closures are choose-your-own-~~adventure~~ ownership.
- Sometimes it’s not okay to have a closure borrow anything.
- You can force a closure to take ownership of all environment
variables by using the
movekeyword. - “Taking ownership” can mean taking a copy, not just moving.
1. let mut magic_num = 5;
2. let own_the_magic = move |x: i32| x + magic_num;
3. let more_magic = &mut magic_num;
Move Closures
moveclosures are necessary when the closurefneeds to outlive the scope in which it was created.- e.g. when you pass
finto a thread, or returnffrom a function. moveessentially disallows bringing references into the closure.
1. fn make_closure(x: i32) -> Box<Fn(i32) -> i32> {
2. let f = move |y| x + y; // ^ more on this in 15 seconds
3. Box::new(f)
4. }
6. let f = make_closure(2);
7. println!("{}", f(3));
Closure Ownership
-
Sometimes, a closure must take ownership of an environment variable to be valid. This happens automatically (without
move): -
If the value is moved into the return value.
1. let lottery_numbers = vec![11, 39, 51, 57, 75]; 2. { 3. let ticket = || { lottery_numbers }; 4. } 5. // The braces do no good here. 6. println!("{:?}", lottery_numbers); // use of moved value- Or moved anywhere else.1. let numbers = vec![2, 5, 32768]; 2. let alphabet_soup = || { numbers; vec!['a', 'b'] }; 3. // ^ throw away unneeded ingredients 4. println!("{:?}", numbers); // use of moved value- If the type is notCopy, the original variable is invalidated.
Closure Ownership
1. let numbers = vec![2, 5, 32768];
2. let alphabet_soup = || { numbers; vec!['a', 'b'] };
3. // ^ throw away unneeded ingredients
4. alphabet_soup();
5. alphabet_soup(); // use of moved value
- Closures which own data and then move it can only be called once.
movebehavior is implicit becausealphabet_soupmust ownnumbersto move it.
1. let numbers = vec![2, 5, 32768];
2. let alphabet_soup = move || { println!("{:?}", numbers) };
3. alphabet_soup();
4. alphabet_soup(); // Delicious soup
- Closures which own data but don’t move it can be called multiple times.
Closure Ownership
- The same closure can take some values by reference and others by moving ownership (or Copying values), determined by behavior.
Closure Traits
- Closures are actually based on a set of traits under the hood!
Fn,FnMut,FnOnce- method calls are overloadable operators.
1. pub trait Fn<Args> : FnMut<Args> {
2. extern "rust-call"
3. fn call(&self, args: Args) -> Self::Output;
4. }
6. pub trait FnMut<Args> : FnOnce<Args> {
7. extern "rust-call"
8. fn call_mut(&mut self, args: Args) -> Self::Output;
9. }
11. pub trait FnOnce<Args> {
12. type Output;
14. extern "rust-call"
15. fn call_once(self, args: Args) -> Self::Output;
16. }
Closure Traits
- These traits all look pretty similar, but differ in the way they take
self: Fnborrowsselfas&selfFnMutborrowsselfmutably as&mut selfFnOncetakes ownership ofselfFnis a superset ofFnMut, which is a superset ofFnOnce.- Functions also implement these traits.
“The || {} syntax for closures is sugar for these three traits. Rust will
generate a struct for the environment, impl the appropriate trait, and then use
it.”¹
¹Taken from the Rust Book
Closures As Arguments
- Passing closures works like function pointers.
- Let’s take a (simplified) look at Rust’s definition for
map¹.
1. // self = Vec<A>
2. fn map<A, B, F>(self, f: F) -> Vec<B>
3. where F: FnMut(A) -> B;
maptakes an argumentf: F, whereFis anFnMuttrait object.- You can pass regular functions in, since the traits line up!
¹Real map coming in next lecture.
Returning Closures
- You may find it necessary to return a closure from a function.
- Unfortunately, since closures are implicitly trait objects, they’re unsized!
1. fn i_need_some_closure() -> (Fn(i32) -> i32) {
2. let local = 2;
3. |x| x * local
4. }
1. error: the trait `core::marker::Sized` is not implemented
2. for the type `core::ops::Fn(i32) -> i32 + 'static`
- An
Fnobject is not of constant size at compile time. - The compiler cannot properly reason about how much space to allocate for the
Fn.
Returning Closures
- Okay, we can fix this! Just wrap the
Fnin a layer of indirection and return a reference!
1. fn i_need_some_closure_by_reference() -> &(Fn(i32) -> i32) {
2. let local = 2;
3. |x| x * local
4. }
1. error: missing lifetime specifier
- Now what? We haven’t given this closure a lifetime specifier…
- The reference we’re returning must outlive this function.
- But it can’t, since that would create a dangling pointer.
Returning Closures
- What’s the right way to fix this? Use a
Box!
1. fn box_me_up_that_closure() -> Box<Fn(i32) -> i32> {
2. let local = 2;
3. Box::new(|x| x * local)
4. }
1. error: closure may outlive the current function, but it
2. borrows `local`, which is owned by the current function [E0373]
- Augh! We were so close!
- The closure we’re returning is still holding on to its environment.
- That’s bad, since once
box_me_up_that_closurereturns,localwill be destroyed.
Returning Closures
- The good news? We already know how to fix this:
1. fn box_up_your_closure_and_move_out() -> Box<Fn(i32) -> i32> {
2. let local = 2;
3. Box::new(move |x| x * local)
4. }
- And you’re done. It’s elementary!
