4

A deep dive into Rust iterators and closures

 2 years ago
source link: https://blog.logrocket.com/rust-iterators-closures-deep-dive/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

A deep dive into Rust iterators and closures

April 15, 2022

5 min read 1477

If you don’t know yet, Rust is an awesome programming language. In fact, it’s so awesome that Stack Overflow voted it the most loved language for the last six years.

Rust is a systems programming language. While languages like C and C++ provide fine-grained control of components like memory management and garbage collection, they make it harder to build modern applications due to syntax complexity.

Meanwhile, languages like Python and JavaScript — while great for developer productivity — lack the performance and security offered by low-level languages.

This is where Rust comes in. Rust combines ease of programming with access to core system-level configurations. It’s built with memory safety, concurrency, and security from the ground up, making it the perfect choice to build scalable high-performance applications.

This article is not an introduction to Rust. I wrote an article recently on getting started with Rust, so check it out if you are new to it.

In this article, we will look at the intermediate Rust concepts of iterators and closures in detail.

(Fun fact: Rust takes double quotes and semicolons very seriously. Coming from a JS/Python background, most of my time learning rust was spent debugging these issues.)

Iterators in Rust

Iteration is the process of looping through a set of values. You might be familiar with loops like “for loop,” “while loop,” and “for each loop.”

In Rust, iterators help us achieve the process of looping. In other languages, you can just start looping through an array of values. In Rust, however, you have to declare an iterator first.

Let’s look at a simple array example. I am going to declare an array of named ages, which we will use throughout this section.

let ages = [27,35,40,10,19];

Now, let’s declare it as a loop-able array by calling the iter() function.

let ages_iterator = ages.iter();

If this looks confusing, don’t worry. You will understand it better once we look at an actual implementation.

Let’s loop through the values and print them out, one by one.

for age in ages_iterator {
  println!("Age = {:?}",age);
}

You can see that we used the ages_iterator instead of the ages vector to perform the looping operation.

All Rust lists (arrays, vectors, maps) are not iterable by default. Using the iter() function, we tell Rust that the given array can be used with a loop.

This is also referred to as iterators being “lazy.” Similar to how a function doesn’t do anything until it is called, the iter() function is used to invoke iteration in an array.

Iterator trait

If you don’t know what Rust traits are, this article will provide you detailed information about traits. Simply put, traits are similar to abstract classes in C++ or interfaces in Java.

Traits are the foundation of abstraction in Rust, and they allow us to define methods that can be shared across different Rust types.

Imagine writing a class which has a function that prints out a summary message. You can invoke this class and call its method to print out different messages, like a Facebook post or a Twitter tweet. Similarly, you can write a trait that will print out a summary and you can implement (inherit) that trait to print a post or a tweet.

In Rust, all iterators implement a trait named “iterator” that has a method called next(). Below is the code from the official Rust documentation on how the iterator trait is defined.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

Don’t worry about the new syntax — all you need to understand is that when you attach the iter() method to make a list of values iterable, it gets the iterator trait implemented along with methods like next().

Let’s look at next() method in detail.

next() method

The next() method is useful when you don’t want to use a loop to go through an iterator. Simply calling this method returns the elements one by one.

Let’s print out our ages array without a loop but using the next() method. Note that we are declaring the iterator as mutable using the mut keyword; this is because all variables in Rust are immutable by default.

fn main(){
    let ages = [27,35,40,10,19];

    let mut ages_iterator = ages.iter();

    // display the iterator
    println!("{:?}",ages_iterator);

    // display each element in array
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());

    // display the iterator
    println!("{:?}",ages_iterator);

    // display the array
    println!("{:?}",ages);
}// example code for using the next method. 

In the above example, we first declare a mutable iterator followed by printing each item in the ages array using the next() method. Here is the output for this code:

Iter([27, 35, 40, 10, 19])
Some(27)
Some(35)
Some(40)
Some(10)
Some(19)
None
Iter([])
[27, 35, 40, 10, 19]

You can see that each next() method returns the first element and then removes it from the array. The last next() method returns None since there are no values left in the array.

You should also note that the next() method clears out the ages_iterator and not the actual array itself. When we print both the iterator and the array, you can see that the iterator is empty but the array is not.

Now that you know how iterators work in Rust, let’s look at how closures are implemented in Rust.

Closures in Rust

Simply put, Closures are anonymous functions that have access to the local scope of a code block even after the execution is out of that code block.

Two things are very important when it comes to closures. They are always anonymous functions (or inline functions) and their main feature is to give us access to a local scope (environment).

Here is the syntax for declaring a closure:

let closure_name = |param1, param2| -> return_type {.....  closure logic …..};

You can see that closure_name is a variable that stores a closure, but the actual closure is an anonymous function.

While you must define the data types for params as well as return values in a function, closures can handle it automatically, but the data type of the values passed to a closure should be consistent or it will throw an error during compile time. To pass parameters to a closure, we use the double pipe symbol (||).

Let’s write a simple closure that increments a given value by one:

fn main() {
    let mut my_val = 0;
    let mut increment_closure = || {
        my_val = my_val + 1;
        println!("Value : {:?}",my_val);
    };
    increment_closure();
    increment_closure();
    increment_closure();
    increment_closure();
    increment_closure();
}

In the above code, you can see that we declare a closure called increment_closure that increments the value of the variable my_val by one and prints the current value of my_val.

Here is the output of this code:

Value : 1
Value : 2
Value : 3
Value : 4
Value : 5

You can see that the closure retains the value of the variable. Notice that we use the mut keyword along with the closure. This is because you have to explicitly tell Rust that the closure is going to modify the environment.

Moving closures

In general, closures create a reference to the entities in its scope, but there is another type of closure called the moving closure that takes ownership of all the variables that it uses.

We use the move keyword to define a moving closure. Here is the syntax:

let closure_name = move |param1, param2| -> return_type {.....  closure logic …..};

Moving closures are used when working with advanced Rust features such as concurrency, so it is out of scope for this article. To learn about Rust closures in depth, check out this article.

Now that you understand how closures work, you might be wondering why we need them in the first place. Closures are primarily used for abstraction.

If you have a global variable that you want to update, any function can modify that variable. However, writing a closure to update a variable ensures that only that closure has access to that variable — this is part of Rust’s design to ensure a highly secure programming language.

Conclusion

Unlike most programming languages, you cannot iterate through a set of values (arrays, vectors, maps) in Rust using loops. We have to call the iter() method to make a list iterable. You can also use the next() method to go through values in a list, one at a time.

Closures are anonymous, inline functions that have access to its environment. They are used to abstract an environment so that the environment’s variables are modified only by limited entities. While normal closures create a reference to the values they work with, moving closures can take ownership of values in an environment.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK