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 semi-columns 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”, etc.
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.
The post Rust: A deep dive into iterators and closures appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/Q81N9X6
via Read more