Focus mode

Rust Programming

(Advanced) Closures

Welcome to the closures for Rust tutorial! In this tutorial, you will learn what closures are, how to create and use them, and why they are awesome. By the end of this tutorial, you will be able to use closures under any circumstances and impress your friends with your Rust skills.

What are closures?

Closures are functions without names that can capture the enclosing environment. They are also known as anonymous functions or lambdas. You might be wondering why you would need a function without a name. Well, there are many situations where you want to create a custom function on the fly, without having to define it separately and give it a name. For example, you might want to pass a function as an argument to another function, or return a function from a function, or store a function in a variable. Closures allow you to do all these things and more.

Unlike functions, closures can capture values from the scope in which they are defined. This means that you can access and manipulate variables that are outside the closure's body. This is very useful for creating dynamic and flexible functions that can adapt to different contexts.

How to create and use closures?

To create a closure in Rust, you use the `||` syntax around the input variables, followed by the body of the closure. For example:

// define a closure to print a text
let print_text = || println!("Defining Closure");


In this example, `print_text` is a variable that stores the closure, and `println!("Defining Closure")` is the body of the closure that prints the text "Defining Closure" when called.

To call a closure, you use the same syntax as calling a function: just add parentheses after the closure's name and pass any arguments inside them. For example:

// call the closure
print_text();


This will print "Defining Closure" to the console.

You can also pass parameters to a closure, just like a function. For example:

// define a closure that takes an integer and returns a boolean
let is_even = |x: i32| -> bool { x % 2 == 0 };


In this example, `is_even` is a closure that takes an integer parameter `x` and returns a boolean value indicating whether `x` is even or not.

You can call this closure with any integer value and get the corresponding result. For example:

// call the closure with different values
println!("Is 2 even? {}", is_even(2)); // true
println!("Is 3 even? {}", is_even(3)); // false


A quick note: In the older versions of the Rust, we did not have to specify the type of the closure and Rust could have infer it. With the newer versions this is not possible.

The Magical Trio: Fn, FnMut, and FnOnce

Now let's talk about Fn, FnMut, and FnOnce, the magical trio of traits that let us use closures in Rust. What are they, exactly?

  • FnOnce: This trait represents closures that can be called exactly once. They may move (consume) values from their environment. It's like a one-time party invitation – you use it, and it's gone.
  • FnMut: This trait is for closures that can be called multiple times and can mutate values from their environment. It's like having a key to the house – you can enter as many times as you want, and you're allowed to move the furniture.
  • Fn: This trait is for closures that can be called multiple times without mutating their environment. It's like being a ghost – you can pass through the house as much as you want, but you can't change anything.

So, when we use these traits as bounds for our generic F in the apply function, we're telling Rust what kind of closure we're expecting. And as always, Rust is pretty understanding about it!

Why are closures awesome?

Closures are awesome because they allow you to create custom functions that suit your needs and adapt to different contexts. You can use them for tasks such as filtering, sorting, mapping, or reducing data collections. You can also use them for implementing callbacks, iterators, or generators.

For example, let's say you have a vector of numbers, and you want to filter out only the even ones. You can use the `filter` method on the vector and pass a closure as an argument that checks if a number is even. For example:

// define a vector of numbers
let numbers = vec![1, 2, 3, 4, 5];

// filter out only the even numbers using a closure
let evens: Vec<i32> = numbers.into_iter().filter(|x| x % 2 == 0).collect();

// print the filtered vector
println!("The even numbers are: {:?}", evens); // [2, 4]


In this example, we use the `into_iter` method to create an iterator over the vector elements, then we use the `filter` method to apply a closure that returns true if `x` is even and false otherwise. Finally, we use the `collect` method to convert the iterator into a vector.

As you can see, closures make it easy and concise to write custom logic for filtering data.

Another example of using closures is for implementing callbacks. A callback is a function that is passed as an argument to another function and is executed at some point during the execution of that function. For example, let's say you have a function that downloads some data from a website and you want to execute some code after the download is complete. You can use a closure as a callback argument and pass it to the download function. For example:

    // define a function that downloads some data from a website
    fn download_data(url: &str, callback: impl FnOnce(&str)) {
        // simulate downloading data by printing some text
        println!("Downloading data from {}...", url);

        // simulate some delay by sleeping for one second
        std::thread::sleep(std::time::Duration::from_secs(1));

        // simulate some data by creating a string
        let data = format!("Some data from {}", url);

        // execute the callback with the data as an argument
        callback(&data);
    }

    // define a closure that prints some text after receiving data
    let print_data = |data: &str| {
        println!("Received data: {}", data);
    };

    // call the download function with a url and a closure as arguments
    download_data("https://www.rust-lang.org", print_data);


In this example, we define a function `download_data` that takes two arguments: a url string and a callback function. The callback function has one parameter of type `&str`, which represents the downloaded data. The callback function also has an `impl FnOnce` trait bound, which means it can be any type of function or closure that takes one `&str` argument and does not return anything.

Inside the `download_data` function, we simulate downloading some data by printing some text and sleeping for one second. Then we create some fake data by formatting a string with the url. Finally, we execute the callback function with the data as an argument.

We also define a closure `print_data` that takes one `&str` argument and prints some text with it. We pass this closure as an argument to the `download_data` function along with a url string.

When we run this code, we get something like this:

Downloading data from https://www.rust-lang.org...
Received data: Some data from https://www.rust-lang.org


As you can see, closures make it easy and concise to write custom logic for callbacks.

Conclusion

In this tutorial, you learned what closures are, how to create and use them, and why they are awesome. You saw how closures allow you to create anonymous functions that can capture values from their environment and adapt to different contexts. You also saw how closures can be used for tasks such as filtering data collections or implementing callbacks.

Closures are one of Rust's most powerful features that enable expressive and elegant code. They are fun and useful, but they can also be tricky and confusing at times. Don't worry if you don't get them right away; practice makes perfect! 😉

Comments

You need to enroll in the course to be able to comment!