Itertools
There is one external crate that can make working with iterators much easier, and gives you superpowers. Remember the idea that these iterators allow you to perform the same operations you would do in C with indexing, but with complete memory safety and zero-cost abstractions? They also make the code much easier to understand. In terms of iterator capabilities, the most important crate is the itertools crate. This crate provides a new trait, the Itertools trait, which gives iterators many new methods and functions that make the life of the developer much easier, while staying true to its core values of performance thanks to zero-cost abstractions. You can add it to your project by adding it to your Cargo.toml file in the [dependencies] section.
Let's explore some of its iterator adapters. We start with a simple one that helps us create batches or chunks of the given iterator, the batching() function. Let's say that we want to use an iterator over one of the previous arrays and we want to make it return elements in groups of three. It's as simple as using that method and creating a closure that directly calls the next() method and returns the required tuple:
// Remember
extern crate itertools;
use itertools::Itertools;
let arr = [10u32, 14, 5, 76, 84, 35, 23, 94, 100, 143, 23, 200, 12, 94, 72];
for tuple in arr.iter().batching(|it| match it.next() {
None => None,
Some(x) => {
match it.next() {
None => None,
Some(z) => {
match it.next() {
None => None,
Some(y) => Some((x, y, z)),
}
}
}
}
})
{
println!("{:?}", tuple);
}
This will print the array in groups of three elements, in order:
(10, 5, 14)
(76, 35, 84)
(23, 100, 94)
(143, 200, 23)
(12, 72, 94)
A similar operation can be accomplished by using the chunks() function. We can say that the batching() adaptor is a generalization of the chunks() adaptor, since it gives you the option to create the internal logic of the function. In the case of chunks(), it will only receive as a parameter the number of elements in a chunk, and it will return slices to those chunks.
A really similar operation will be performed with the tuples() method. As you can see, the batching() method is a complete generalization in terms of how you create batches or chunks of an iterator. Let's see the same example we saw previously using the tuples() method:
// Remember
extern crate itertools;
use itertools::Itertools;
let arr = [10u32, 14, 5, 76, 84, 35, 23, 94, 100, 143, 23, 200, 12, 94, 72];
for tuple in arr.iter().tuples::<(_, _, _)>() {
println!("{:?}", tuple);
}
Much less boilerplate code, right? In this case, we are required to specify the number of elements in a tuple, but if we used type inference in the for, we could avoid it:
// Remember
extern crate itertools;
use itertools::Itertools;
let arr = [10u32, 14, 5, 76, 84, 35, 23, 94, 100, 143, 23, 200, 12, 94, 72];
for (a, b, c) in arr.iter().tuples() {
println!("({}, {}, {})", a, b, c);
}
Of course, in this case, we would be pattern-assigning the variables. There is also another interesting function that allows for creating the cartesian product of two iterators.
Unsurprisingly, the name is cartesian_product(). This will create a new iterator with all possible combinations of the previous two:
// Remember
extern crate itertools;
use itertools::Itertools;
let arr1 = [10u32, 14, 5];
let arr2 = [192u32, 73, 44];
for row in arr1.iter().cartesian_product(arr2.iter()) {
print!("{:?}, ", row);
}
This will print the following:
(10, 192), (10, 73), (10, 44), (14, 192), (14, 73), (14, 44), (5, 192), (5, 73), (5,44),
There are many other methods in the Itertools trait, and I invite you to check the official documentation, since it's very detailed and has many examples. For now, these common methods should help you do any operation you need to perform with slices in a much more efficient way.