DEV Community

Cover image for What The Closure!? Closures In Rust Explained
Confidence Okoghenun
Confidence Okoghenun

Posted on • Originally published at confidence.sh

What The Closure!? Closures In Rust Explained

Closures in Rust look a bit weird, and it’s not just their syntax, they behave differently from what you may expect too. They also share similarities with functions, making them easy to learn but sometimes confusing.

This article clearly explains what closures in Rust are, and how to use them in practice.

Closure Syntax

Let's start by tackling one of the weird parts about closures; their syntax. They have a unique syntax, and unlike most Rust code, they don’t need types as the compiler infers it. Other than that, function and closures are quite similar. Take a look at the example below:

//function
fn add(a: i32, b: i32) -> i32 {
    a + b
}

//closure with types
|a: i32, b: i32| -> i32 {
    a + b
};

//closure without types
|a, b| {
    a + b
};

//closure compact. most popular!
|a, b| { a + b };
Enter fullscreen mode Exit fullscreen mode

While Closures are usually untyped, nothing stops you from adding types. Though types are omitted for brevity, the compiler can infer the types from how the closure is used.

You may also have noticed closures don’t have names. That’s because they are anonymous functions designed to be non-public (i.e used only in your code for use in simple computations). That said, you can assign closures to variables to facilitate reuse.

let add = |a, b| { a + b };
Enter fullscreen mode Exit fullscreen mode

Now you can call add can multiple times just like a function.

Closures Capture Environment

Meme
Another difference between closures and functions is; closures can capture variables from their environment, while functions can't. As an example, imagine we wanted to print name:

let name = String::from("john");

fn fn_print() {
    println!("{}", name);
}

fn_print();
//⚠️⚠️⚠️
//error[E0434]: can't capture dynamic environment in a fn item
Enter fullscreen mode Exit fullscreen mode

This doesn’t compile because functions can see variables outside their scope. The variables have to be explicitly passed as arguments. Not so for closures:

let name = String::from("mary");
let cl_print = || println!("{}", name);
cl_print();
//compiles like a charm ✨
Enter fullscreen mode Exit fullscreen mode

It’s important to know that variables captured by closures may be borrowed mutably or immutably as per Rust conventions. As expected, this will affect the validity of that variable after the closure. Additionally, a variable may be moved into the closure using the move keyword. For example:

let name = String::from("john");
std::thread::spawn(move || println!("{}", name)).join().unwrap();
Enter fullscreen mode Exit fullscreen mode

In the above example, println runs in a new thread so it makes sense to move name as it's not subsequently used in the main thread.

Where are Closures used?

Closures are great for doing computations, especially when you need to capture values from the environment. They can have parameters, and can be passed as arguments to other functions. You may have come across closures in methods on types i.e on map :

let v = vec![1, 2, 3];
let v = v.into_iter().map(|x| x + 1).collect::<Vec<u8>>();
println!("{:#?}", v);
Enter fullscreen mode Exit fullscreen mode

…or unwrap_or_else :

let num = Some(10).unwrap_or_else(|| -1);
println!("{:#?}", num);
Enter fullscreen mode Exit fullscreen mode

Or on any method that takes a function or closure implementing any one of the fn traits. What are fn traits?

Closure Traits

Closures implement one of three traits depending on what they do to captured values:

  1. FnOnce - Applies to closures that can be called once i.e all closures. Closures that move captured values out of their body can’t implement other fn traits.
  2. FnMut - Closures that can mutate captured values and be called multiple times.
  3. Fn - Closures that neither move nor mutate captured values, allowing multiple concurrent calls

It’s good to keep this in mind especially when working with trait bounds

Conclusion

Closures are quite handy and fun, fun, functional. They capture their environment and don’t need explicit type annotations, making them even more convenient. That’s it on closures. If you’d love to learn more about Rust, connect with me on LinkedIn or Twitter. Bye!

Top comments (0)