Closures

In Rust, closures are akin to "anonymous functions" in other programming languages. You can define a closure and assign it to a Rust variable. After you've defined the variable pointing to a closure, you can invoke that function later on.

Just like regular functions, closures support input arguments and return types.

The syntax for defining a closure is somewhat different to defining a regular Rust function. The key differences are that:

  • Closure input arguments are surrounded by the pipe character | |, instead of parentheses
  • Closures don't require the use of curly braces to surround the function body
  • Closure return types don't need to be declared

Basic Closure

Here's a basic version of a closure. The empty pipe characters mean that there are no input arguments defined. The function body only has a single statement, and doesn't require curly braces surrounding it.

#![allow(unused)]
fn main() {
let myfn = || println!("Hello from Rust"); // Define the closure
myfn(); // Call the closure
}

Input Arguments

If you want to define input arguments on a closure, add them between the pipe characters.

#![allow(unused)]
fn main() {
let fn_full_name = | first_name: String, last_name: String | println!("{} {}", first_name, last_name);
fn_full_name("Trevor".to_string(), "Sullivan".to_string());
}

Closure Return Values

If you need to return a value from a closure, you don't need to specify the return keyword. The following example will return the result from the format! macro.

#![allow(unused)]
fn main() {
let fn_full_name = | first_name: String, last_name: String | format!("{} {}", first_name, last_name);
let full_name = fn_full_name("Trevor".to_string(), "Sullivan".to_string());
println!("{full_name}");
}

Multi-line Closure Body

While the previous closure examples only contain a single statement, Rust closures can support complex bodies as well. Simply use curly braces to denote the closure body, just like you would with functions.

#![allow(unused)]
fn main() {
let fn_full_name_lower = | first_name: String, last_name: String | {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower("Trevor".to_string(), "Sullivan".to_string());
println!("{full_name_lower}");
}

Capture Environment

Rust closures capture the variables defined in the same scope as the closure definition. This means that variables defined outside the closure can be referenced inside the closure.

Consider the following example.

#![allow(unused)]
fn main() {
let first_name = "Trevor".to_string();
let last_name = "Sullivan".to_string();
let fn_full_name_lower = || {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower();
println!("{full_name_lower}");
}

Rather than passing the first_name and last_name as input arguments, we can define them in the same scope as the closure. When we call the function, the values of the variables are accessible inside the closure body.

Using the Move Keyword

If you don't need to access a variable after the closure is defined, you can use the move keyword to move ownership of the variable into the closure itself.

If you try the following example, you'll receive an error message from the Rust compiler saying "borrow of moved value." That's because ownership of the first_name variable was moved into the closure, and after the closure's scope is dropped, the first_name variable is no longer valid.

#![allow(unused)]
fn main() {
let first_name = "Trevor".to_string();
let last_name = "Sullivan".to_string();
let fn_full_name_lower = move || {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower();
println!("{full_name_lower}");
println!("{first_name}");
}