Closures

If you know about closures already, this chapter will hold no big surprises for you, they function about how you would expect. However, some Rust features rely heavily on them, so of course there's a chapter for them. If you don't know what closures are, they go by "lambdas" in some languages like C++.

In short, they are anonymous functions which can capture parts of the scope they are defined in. They are written as two pipes with their arguments in between, followed by an expression evaluating to data of their return type. Argument types and return type should be able to be inferred in almost all cases, but you can also make them explicit if you want to. Their main use is in customising behaviour of some functions, which we will see extensively shortly. Depending on what they capture from their environment, they implement one of three traits:

  • Fn doesn't capture anything at all, or only immutable references. Regular functions implement this trait, too. Closures implementing this can be used where the following traits are expected, as well.
  • FnMut captures mutable references of the environment, so it can mutate them. Closures implementing this can be used anywhere where FnOnce is expected, too.
  • FnOnce fully captures and consumes parts of the environment, as it moves it into itself. As the name and the ownership rules imply, this type of closure can only be called once. Closures implementing this have a move keyword in front of it.

One important use is in spawning a thread. As spawned threads start working immediately, they don't take functions with arguments (which would immediately need those arguments anyway), but instead a closure that can capture any items the thread needs.


#![allow(unused)]
fn main() {
let (sender, receiver) = std::sync::mpsc::channel();

// Closure captures sender and returns a boolean
let thread = std::thread::spawn(move || {
  if let Ok(()) = sender.send(String::from("Hello world!")) {
    true
  } else {
    false
  }
});

let success = thread.join().unwrap();

if success {
  println!("Message from thread: {}", receiver.recv().unwrap());
}
}

The second important use case is when sorting or searching for items. When they don't have Ord implemented, but contain a value that does and you want to use that, you can use the *_by_key version of the respective function. It accepts a closure that takes an item reference as its argument and returns one that implements the trait. You can also define the entire comparison function yourself by using the *_by versions of those functions. Of course, we're going to need a nice little example for that, don't we?


#![allow(unused)]
fn main() {
use std::cmp::Ordering;

struct MyStruct {
  pub id: usize,
  pub some_data: usize,
}

// Let's just assume that we get some
// data from somewhere else
fn example(data: &mut [MyStruct]) {
  // Doesn't work, we don't have the
  // Ord trait implemented!
  data.sort();
  // Assume we only want to sort by the ID
  data.sort_by_key(|item| item.id);
  // Assume we want to sort by ID first
  // and data second
  data.sort_by(|first, second| {
    match first.id.cmp(&second.id) {
      Ordering::Equal => first.some_data.cmp(&second.some_data),
      other => other
    }
  });
}
}

The third important use case is iterators, which we will go into now.