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 {}.

???

  • let instead of fn
  • Arguments in pipes
  • Braces are optional

Type Inference

  1. let square_v4 = |x: u32| { (x * x) as i32 };
  2. let square_v4 = |x| -> i32 { x * x }; // ← unable to infer enough
  3. 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 x from the return expression x * 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_magic is able to reference magic_num even though it’s not passed as an argument.
    • magic_num is in the “environment” of the closure.
    • magic_johnson is not borrowed!

Closure Environment

  • If we try to borrow magic_num in 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;
  4. let more_magic = &mut magic_num; // Err!
  5. println!("{}", magic_johnson); // Ok!
  1. error: cannot borrow `magic_num` as mutable because it is
  2. already borrowed as immutable
  3. [...] the immutable borrow prevents subsequent moves or mutable
  4. borrows of `magic_num` until the borrow ends
  • Why? plus_magic borrows magic_num when it closes over it!
  • However, magic_johnson is 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
  5. let more_magic = &mut magic_num; // Ok!
  6. 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 move keyword.
    • “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

  • move closures are necessary when the closure f needs to outlive the scope in which it was created.
    • e.g. when you pass f into a thread, or return f from a function.
    • move essentially 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. }
  5. let f = make_closure(2);
  6. 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 not Copy, 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.
    • move behavior is implicit because alphabet_soup must own numbers to 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. }
  5. pub trait FnMut<Args> : FnOnce<Args> {
  6. extern "rust-call"
  7. fn call_mut(&mut self, args: Args) -> Self::Output;
  8. }
  9. pub trait FnOnce<Args> {
  10. type Output;
  11. extern "rust-call"
  12. fn call_once(self, args: Args) -> Self::Output;
  13. }

Closure Traits

  • These traits all look pretty similar, but differ in the way they take self:
    • Fn borrows self as &self
    • FnMut borrows self mutably as &mut self
    • FnOnce takes ownership of self
  • Fn is a superset of FnMut, which is a superset of FnOnce.
  • 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;
  • map takes an argument f: F, where F is an FnMut trait 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 Fn object 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 Fn in 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_closure returns, local will 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!