The Rust Programming Language - Chapter 13 functional language features in the Rust language: iterators and closures - 13.2 using iterators to process element sequences

13 functional language functions in rust language: iterators and closures

Functional programming style usually includes taking a function as a parameter and return value of another function, and assigning a function as a value to a variable for subsequent execution

In this chapter, we will introduce the following:

Closure: a function like data structure that can be stored in variables

Iterator: a way to process a sequence of elements

How do you use these features to improve the I/O projects in Chapter 12

Performance of these two functions (spoiler warning: they are faster than you think)

My 40m broadsword is already hungry. Let's conquer this chapter!

13.2 processing element sequences using iterators

The iterator pattern allows us to do some processing on the items of a sequence. The iterator is responsible for traversing each item in the sequence and determining the sequence and the logic that determines when the sequence ends. When using iterators, we do not need to re implement these logic

In Rust, iterators are lazy, which means they will not work until the calling method uses them

let v1 = vec![1,2,3];
let v1_iter = v1.iter();

Using iterators in for loops reduces duplication of code and eliminates potential confusion

fn main(){

let v1 = vec![1,2,3];
let v1_iter = v1.iter();

for val in v1_iter {
    println!("got: {}",val);
    }
}

In addition, the implementation of iterators provides the flexibility to use the same logic for a variety of different sequences, not just indexable data structures such as vector s. Let's see how iterators do this

Iterator trail and next methods

Iterators implement a trait defined in the standard library called Iterator, which looks as follows:

pub fn trait Iterator{
    type Item;
    
    fn next(&mut self)->Option<Self::Item>;

    //The default implementation of the method is omitted here
}

Here is a new syntax: type Item and Self::Item. They define the association type of trait. We will talk about this later. We only need to know that this code indicates that to implement Iterator, we must define an Item type. This Item type is regarded as the return value type of next method. In other words, the Item type will be the type of element returned by Iterator

next is the only method that the Iterator implementer is required to define. next returns one item at a time, encapsulated in Some. When the Iterator ends, it returns None

You can call the iterator's methods directly

#[test]
fn iterator_demonstration() {
    let v1 = vec![1,2,3];
    let mut v1_iter = v1.iter();
    
    assert_eq!(v1_iter.next(),Some(&1));
    assert_eq!(v1_iter.next(),Some(&2));
    assert_eq!(v1_iter.next(),Some(&3));
    assert_eq!(v1_iter.next(),None);
}
running 1 test
test iterator_demonstration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Method of consuming iterators

Iterator trait has many different default implementation methods, which are provided by the standard library and can be viewed in the standard library API document. Some methods call the next method in its definition, which is why what is required to implement next methods when implementing Iterator trait.

These methods that call the next method are called consumption adapters because calling them consumes iterators. A typical method is sum. This method takes ownership of the iterator and repeatedly calls next to traverse the iterator, thus consuming the iterator

#[test]
fn iterator_sum() {
    let v1 = vec![1,2,3];
    let mut v1_iter = v1.iter();
    let total:i32 = v1_iter.sum();
    assert_eq!(total,6);   
}
running 1 test
test iterator_sum ... ok

Method of generating other iterators

Another class of methods, called iteration adapters, is defined in Iterator trait, which allows us to change the current iterator into different types of iterators. Multiple iterator adapters can be chained. However, because iterators are lazy, you must call a consumption adapter method to get the result of the iterator adapter call

As follows, the map method uses closures to call each element to generate a heart iterator. The closures here create a new iterator in which each element in the vector is + 1

#[test]
fn add_one() {
    let v1:Vec<i32> = vec![1,2,3];

    let v2:Vec<_>=v1.iter().map(|x|x+1).collect();

    assert_eq!(v2,vec![2,3,4]);
}
running 1 test
test add_one ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Get environment using closures

Now that we've introduced iterators, let's show a general use case that captures the closure of the environment through the filter iterator adapter

#[derive(PartialEq,Debug)]
struct Shoe{
    size:u32,
    style:String,
}

fn shoes_in_my_size(shoes:Vec<Shoe>,shoe_size:u32)->Vec<Shoe> {
    shoes.into_iter()
    .filter(|s|s.size == shoe_size)
    .collect()
}

#[test]
fn filters_by_size(){
    let shoes = vec![
        Shoe{size:10,style:String::from("sneaker")},
        Shoe{size:13,style:String::from("sandal")},
        Shoe{size:10,style:String::from("boot")},
    ];
    let in_my_size = shoes_in_my_size(shoes, 10);

    assert_eq!(
        in_my_size,
        vec![
            Shoe{size:10,style:String::from("sneaker")},
            Shoe{size:10,style:String::from("boot")},
        ]
    );
}
running 1 test
test filters_by_size ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Implement Iterator trait to create custom iterators

We can call iter and iter on the vector_ iter or iter_mut to create an iterator, or you can create an iterator with other collection types in the standard library, such as hash map. In addition, Iterator trait can be implemented to create any iterator we want. As mentioned earlier, the only method required in the definition is the next method. Once it is defined, you can create a custom iterator with all other methods with default implementation provided by Iterator trait

As an illustration, we create an iterator counting from 1 to. First, we create a structure to store some values, then implement Iterator trait, put the structure into the iterator and use its value in this implementation

struct Counter {
    count:u32,
}
impl Counter {
    fn new()-> Counter{
        Counter{count:0}
    }
}

Create a structure and associated function, then we implement Iterator trait for Counter type, and specify the behavior when using iterators by defining the next method

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self)->Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        }else {
            None
        }
    }
}

Here, setting the iterator's association type Item to u32 means that the iterator will return a collection of u32 values. Again, there is still no need to worry about association types

We want the iterator to add one to its internal state, which is why count is initialized to 0: we want the iterator to return 1 first. If the count value is less than 6, next will return the current value encapsulated in Some, but if the count is greater than or equal to 6, the iterator will return None

next method using Counter iterator

Once Iterator trait is implemented, we have an iterator!

#[test]
fn calling_next_directory(){
    let mut counter = Counter::new();

    assert_eq!(counter.next(),Some(1));
    assert_eq!(counter.next(),Some(2));
    assert_eq!(counter.next(),Some(3));
    assert_eq!(counter.next(),Some(4));
    assert_eq!(counter.next(),Some(5));
    assert_eq!(counter.next(),None);

}
running 1 test
test calling_next_directory ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Test passed!

Use other Iterator trait methods in the custom iterator

By defining the next method to implement the Iterator trait, we can now use the Iterator trait method with the default implementation defined by any standard library, because they all use the function of the next method

#[test]
fn using_other_interator_trait_methods(){
    let sum:u32 = Counter::new().zip(Counter::new().skip(1))
                                .map(|(a,b)| a*b)
                                .filter(|x| x%3 == 0)
                                .sum();
    assert_eq!(18,sum)
}
running 1 test
test using_other_interator_trait_methods ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

There are several ways to use a custom Counter iterator

Note: the zip value produces four pairs of values: theoretically, the fifth pair of values (5, None) has never been generated, because zip returns None when any input iterator returns None

All of these method calls are possible because we specify how the next method works, while the standard library provides a default implementation of other methods that call next

Tags: Rust test

Posted on Sun, 21 Nov 2021 14:21:13 -0500 by andreash