Functional Programming: Closures and Iterators in Rust

Closure Closure

Closure isan anonymous function that can be assigned to a variable or passed as a parameter to other functions. Unlike functions, it allows capturing the value in the caller scope, for example:

fn main() {<!-- -->
   let x = 1;
   let sum = |y| x + y;

    assert_eq!(3, sum(2));
}

The above code shows a very simple closure sum that takes an input parameter y and captures the value of x in the scope. , so calling sum(2) means adding 2 (parameter y) and 1 (x), and finally returning their sum :3.

You can see that sum fits the definition of a closure very well: it can be assigned to a variable, allowing the capture of the value in the caller’s scope.

Rust closures draw lessons from the Smalltalk and Ruby languages in form. The biggest difference from functions is that their parameters are in the form of |parm1| Make a declaration. If there are multiple parameters, just |param1, param2,...|. The formal definition of closure is given below:

|param1, param2,...| {<!-- -->
    Statement 1;
    Statement 2;
    return expression
}

If there is only one return expression, the definition can be simplified to:

|param1| Return expression

The following shows the function and closure implementation of the same function:

fn add_one_v1 (x: u32) -> u32 {<!-- --> x + 1 }
let add_one_v2 = |x: u32| -> u32 {<!-- --> x + 1 };
let add_one_v3 = |x| {<!-- --> x + 1 };
let add_one_v4 = |x| x + 1;

Note: The value returned by the last line of expression in the closure is the return value after the closure is executed.

Iterator Iterator

Iterators allow us to iterate a continuous collection, such as arrays, dynamic arrays Vec, HashMap, etc. During this process, we only need to care about how the elements in the collection are processed, and There is no need to worry about how to start, how to end, what index to access, etc.

The IntoIterator trait has an into_iter method, so we can also explicitly convert an array into an iterator:

let arr = [1, 2, 3];
for v in arr.into_iter() {<!-- -->
    println!("{}", v);
}

The reason why an iterator becomes an iterator is because it implements the Iterator feature. To implement this feature, the most important thing is to implement the next method. , this method controls how to get the value from the collection, and the type of the final return value is the associated type Item.

Example: Simulate implementation of for loop

Because the for loop is syntactic sugar for iterators, we can completely simulate it through iterators:

let values = vec![1, 2, 3];

{<!-- -->
    let result = match IntoIterator::into_iter(values) {<!-- -->
        mut iter => loop {<!-- -->
            match iter.next() {<!-- -->
                Some(x) => {<!-- --> println!("{}", x); },
                None => break,
            }
        },
    };
    result
}

IntoIterator::into_iter uses a fully qualified method to call the into_iter method. This calling method is equivalent to values.into_iter() .

At the same time, we use the loop loop with the next method to traverse the elements in the iterator. When the iterator returns None, break out of the loop.

You can use into_iter to convert an array into an iterator. In addition, there are iter and iter_mut. Their differences are as follows:

  • into_iter will take away ownership
  • iter is borrowed
  • iter_mut is a mutable borrow
The difference between Iterator and IntoIterator

The two are actually quite easy to confuse, but we just need to remember that Iterator is the iterator feature. Only when it is implemented can it be called an iterator and next can be called. >.

And IntoIterator emphasizes that if a certain type implements this feature, it can become an iterator through methods such as into_iter and iter.

Use collect to collect into a HashMap collection:

use std::collections::HashMap;
fn main() {<!-- -->
    let names = ["sunface", "sunfei"];
    let ages = [18, 18];
    let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();

    println!("{:?}",folks);
}

zip is an iterator adapter. Its function is to compress the contents of two iterators together to form a new one like Iterator The iterator here is an iterator in the form of [(name1, age1), (name2, age2)].

Then use collect to collect the values in the form of (K, V) in the new iterator into HashMap. Similarly, The type must be declared explicitly here, and then the internal KV type of HashMap can be given to the compiler to deduce, and finally the compiler will deduce HashMap< & amp;str , i32>, absolutely correct!

Closure as adapter parameter

In the previous map method, we used closures as parameters of the iterator adapter. Its biggest advantage is not only that it can process elements in the iterator in place, but also that it can capture environment values:

struct Shoe {<!-- -->
    size: u32,
    style: String,
}

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

filter is an iterator adapter that filters each value in the iterator. It takes a closure as a parameter, the parameter s of the closure is the value from the iterator, and then uses s with the shoe_size from the external environment > Compare, if equal, keep the s value in the iterator, if not, remove the s value from the iterator, and finally pass collect is collected as Vec type.

Implement Iterator feature

In the previous content, we have been creating iterators based on arrays. In fact, not only arrays, but also iterators can be created based on other collection types, such as HashMap. You can also create your own iterators – just implement the Iterator trait for your custom type.

First, create a counter:

struct Counter {<!-- -->
    count: u32,
}

impl Counter {<!-- -->
    fn new() -> Counter {<!-- -->
        Counter {<!-- --> count: 0 }
    }
}

We implement an associated function new for the counter Counter, which is used to create a new counter instance. Next we continue to implement the Iterator feature for the counter:

impl Iterator for Counter {<!-- -->
    type Item = u32;

    fn next( & amp;mut self) -> Option<Self::Item> {<!-- -->
        if self.count < 5 {<!-- -->
            self.count + = 1;
            Some(self.count)
        } else {<!-- -->
            None
        }
    }
}

First, set the associated type of the feature to u32. Since the count field saved by our counter is of type u32, so in In the next method, the final returned value is actually the Option type.

Each time the next method is called, the counter value will be incremented by one, and then the latest count value will be returned. Once the count is greater than 5, None will be returned.

Finally, use our new Counter to iterate:

 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);
Other ways to implement the Iterator trait

It can be seen that implementing your own iterator is very simple, but the Iterator feature has more than just one method: next, so why do we only need to implement it? Because other methods have default implementations, there is no need to manually implement them like next, and these default implementation methods are actually implemented based on the next method.

The following code demonstrates the use of some 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);

Where zip, map, filter are iterator adapters:

  • zip Merges two iterators into one iterator. Each element in the new iterator is a tuple, consisting of the elements of the previous two iterators. For example, merge iterators of form [1, 2, 3, 4, 5] and [2, 3, 4, 5] After that, the new iterator is in the shape of [(1, 2),(2, 3),(3, 4),(4, 5)]
  • map converts the values in the iterator into new values [2, 6, 12, 20] after mapping
  • filter filters the elements in the iterator. If the closure returns true, the elements [6, 12] will be retained, otherwise they will be eliminated.

And sum is a consumer adapter that sums all elements in the iterator and ultimately returns a u32 value 18.