Closures

Previously, we looked at named functions and we looked at ownership. In Rust, we also have closures, which give us the ability to make anonymous functions.

You can make a closure with explicit or inferred types:

#![allow(unused)]
fn main() {
let y: u32 = 10;
let annotated = |x: u32| -> u32 { x + y };
let inferred = |x| x + y;

println!("annotated: {}", annotated(32));
println!("inferred: {}", inferred(32));
}

The basic syntax for a closure is to use pipes around the parameter list followed by an expression for the return value. Sometimes you'll see a no-argument closure, which looks like it's using the or-operator (||) but that's the empty parameter list here, and could be written with spaces for clarity (| |).

Closures can reference values from their outer scope, which is really handy. They can also capture the outer values and use them. This is handy for things like counters:

#![allow(unused)]
fn main() {
let mut count = 0;
let mut increment = || {
    count += 1;
    count
};

println!("count is {}", increment());
println!("count is {}", increment());
println!("count is {}", increment());
}

Note that the closure must be mut if it captures a mutable variable. This is because what it captures is part of the closure, so if it's mutating it then it's mutating itself.

You can also return closures from functions! To do this, if you capture variables, you'll need to move the variables into the closure. You do this with the move keyword, which signals to the compiler that it should take ownership of its arguments, so that the closure cannot outlive its arguments.

Here are two examples of that in action. First, a closure which prints a message. This one has to explicitly annotate its lifetime, including on the return type.

#![allow(unused)]
fn main() {
fn print_msg<'a>(msg: &'a str) -> impl Fn() + 'a {
    let printer = move || {
        println!("{msg}");
    };
    printer
}
}

The way you would use it is by calling it to get a function, then calling that function.

#![allow(unused)]
fn main() {
// this line creates a new function, f
let f = print_msg("hello, world");
// nothing has been printed yet

// and this line invokes the function, which will print our message
f();
}

And one which makes a counter.

#![allow(unused)]
fn main() {
fn make_counter() -> impl FnMut() -> u32 {
    let mut count = 0;
    let increment = move || {
        count += 1;
        count
    };
    increment
}
}

Invoking it is similar, but this time, it has to be mutable.

#![allow(unused)]
fn main() {
let mut counter = make_counter();

println!("count is {}", counter()); // prints 1
println!("count is {}", counter()); // prints 2
println!("count is {}", counter()); // prints 3
}

You'll notice the return types for these functions are different. What they return is an impl of a trait. We'll get to what traits are in a later section; for now, you can think of them like interfaces, so we know what we can do with the thing, but not its specific type.

There are three traits for functions: Fn, FnMut, and FnOnce, which provide various restrictions on how the caller of the function can use it. The other thing you'll notice is the impl keyword, which is new. This says we'll return something which implements this trait (like an interface in other languages), but we don't specify exactly what it is. This is how you return closures, generally, because each closure is its own type.

There are good docs on these traits, as usual. In short, the restrictions are:

  • Fn can be called be called multiple times, and it doesn't modify its underlying state.
  • FnMut can be called multiple times, but it may mutate itself when you do (so it needs a mutable reference to itself)
  • FnOnce can be called once. It consumes itself in that call, and you can't use it a second time.

If you have an Fn, you can use it as FnMut or FnOnce. And you can use FnMut as FnOnce. But you can't go back up the chain!

Exercises:

  1. Write a closure which takes in two numbers and adds them together.
  2. Write a function which takes in an initial value and an increment and returns a closure which increments the value by that amount each time it's called.

Thanks for following along so far! You've gotten through what I think are the hardest parts of Rust. The rest should be easier, and you should be able to put this into practice. Take a breather, then move on to the next section.