[Learn Rust programming with Xiaojia] Thirteen, functional language features: iterators and closures

Series Article Directory

[Learn Rust programming with Xiaojia] 1. The basics of Rust programming
[Learn Rust programming with Xiaojia] 2. Use of Rust package management tools
[Learn Rust programming with Xiaojia] 3. Basic program concepts of Rust
[Learn Rust programming with Xiaojia] 】4. Understand the concept of ownership in Rust
【Learn Rust programming from Xiaojia】5. Use structures to associate structured data
【Learn Rust programming from Xiaojia】6. Enumeration and pattern matching
【Learn Rust programming from Xiaojia】7. Use packages (Packages), unit packages (Crates) and modules (Module) to manage projects
[Learn Rust programming with Xiaojia] 8. Common collections
[Learn Rust programming with Xiaojia] 9. Error handling (Error Handling)
[Follow Xiao Jia learns Rust programming] 11. Write automated tests
[Learn Rust programming with Xiao Jia] 12. Build a command line program
[Learn Rust programming with Xiao Jia] 13. Functional language features: iterators and closures

foreword

Rust's design was inspired by many existing languages ​​and technologies, one notable influence being functional programming . Functional programming styles often include functions as parameter values ​​or return values ​​of other functions, assigning values ​​to variables for later execution, and so on.

The main teaching material refers to "The Rust Programming Language"


1. Closures

1.1. Closures

Closures in Rust are anonymous functions that can be stored in variables or passed as arguments to other functions. Closures can be created in one place and then performed in a different context. Unlike functions, closures allow capturing values ​​in the calling scope.
For example:

  • Pass functions as parameters
  • Using functions as function return values
  • assign function to variable

1.2, Rust's closure syntax

1.2.1, closure syntax form

Rust closure borrows from Smalltalk and Ruby in form. The biggest difference from functions is that its parameters are declared in the form of |param|.

Example: Closure syntax form

|param1,param2,....|{
    
    
	语句1;
	语句2...
	返回表达式
}

1.2.1. Simplified form of closure

If there is only one return expression, it can be simplified to the following form

|param|返回表达式

1.3. Type deduction of closures

Rust is a static language, so all variables have types, but thanks to the powerful type deduction ability of the compiler, in many cases we do not need to explicitly declare types, but functions and must specify types for all parameters and return values.

In order to increase the readability of the code, sometimes we will explicitly mark the type. For the same purpose, we can also mark the type of the closure.

let sum = |x:i32, y:32| -> 32{
    
    
	x + y
}

Although type deduction is very useful, it is not generic. When the compiler deduces a type, it will always use that type.

1.4. Closures in structures

struct Cacher<T> where T: Fn(u32) -> u32 {
    
    
	query: T,
	value: Optional<u32>
}

At this time, query is a closure, and its type is Fn(u32) -> u32 is a feature, which is used to indicate that T is a closure type.

1.5, capture the value in the environment

1.5.1, capture the value of the environment

Closures can capture values ​​in the environment

fn main() {
    
    
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

If you use functions to implement, the compiler will tell us that it cannot capture the value in the dynamic environment.

1.5.2, the impact of closures on memory

When a closure captures a value from the environment, it allocates memory to store those values. For some scenarios, this extra memory allocation can become a burden. In contrast, functions do not capture these environment values, so defining and using functions does not have this memory burden.

1.5.2. Three Fn traits

There are three ways for closures to capture environment variables, which correspond to the three ways of passing in function parameters: transfer ownership, variable borrowing, and immutable borrowing, so there are three types of Fn Trait.

1.5.2.1、FnOnce

Closures of this type take ownership of captured variables. The closure can only be run once.

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool,
{
    
    
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    
    
    let x = vec![1, 2, 3];
    fn_once(|z|{
    
    z == x.len()})
}

At this time, the compiler will report an error, because it cannot make a second call to the closure variable that has lost ownership. The error message tells us that because F does not implement Copy Trait, it will report an error, then we add constraints and try to implement the closure of Copy.

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
    
    
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    
    
    let x = vec![1, 2, 3];
    fn_once(|z|{
    
    z == x.len()})
}

If you want to force the closure to take ownership of the captured variable, you can add the move keyword in front of the parameter list. This usage is usually used when the life cycle of the closure is longer than the life cycle of the captured variable, such as returning the closure or moving it into another thread.

use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    
    
    println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();

1.5.2.2、FnMut

It's a mutable borrow that captures a value in the environment so that it can be modified.

fn main() {
    
    
    let mut s = String::new();

    let mut update_string =  |str| s.push_str(str);
    update_string("hello");

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

complex form

fn main() {
    
    
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

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

fn exec<'a, F: FnMut(&'a str)>(mut f: F)  {
    
    
    f("hello")
}

1.5.2.3、Fn Stroke

It captures the value in the environment in the form of immutable borrowing. Let us change the F type in the above code to Fn.

fn main() {
    
    
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

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

fn exec<'a, F: Fn(&'a str)>(mut f: F)  {
    
    
    f("hello")
}

It is clear from the error report that our closure implements the FnMut feature and needs variable borrowing, but it is marked with the Fn feature in exec, so there is a mismatch. Let’s take a look at the correct one. Change the borrowing method:

fn main() {
    
    
    let s = "hello, ".to_string();

    let update_string =  |str| println!("{},{}",s,str);

    exec(update_string);

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

fn exec<'a, F: Fn(String) -> ()>(f: F)  {
    
    
    f("world".to_string())
}

1.5.3, move and Fn

Above we explained the importance of the move keyword to FnOnce. In fact, closures using move may still implement the Fn and
Fn Mut features.

Because which Fn trait a closure implements depends on how the closure uses the captured variables, not on how the closure captures them. move itself emphasizes the latter, how closures capture variables:

1.5.4, the relationship between the three Fn

In fact, a closure not only implements a Fn trait, the rules are as follows

  • All closures automatically implement the FnOnce trait, so any closure can be called at least once
  • Closures that do not remove ownership of all captured variables automatically implement the FnMut Trait
  • Closures that do not require changes to captured variables are automatically invalidated Fn Trait
pub trait Fn<Args> : FnMut<Args> {
    
    
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    
    
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    
    
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

1.6. Closure as function return value

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
    
    
    let num = 5;

    if x > 1{
    
    
        Box::new(move |x| x + num)
    } else {
    
    
        Box::new(move |x| x - num)
    }
}

2. Iterators

2.1. Iterators

Iterator pattern: Perform some action on a sequence of items, and the iterator is responsible for traversing each item, determining when the sequence is complete. Rust's iterators are lazy, and the iterator itself has no effect unless the method that consumes the iterator is called.

Iterators allow us to iterate over a contiguous collection such as an array, vector, hashMap, etc.

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

let v1_iter = v1.iter();

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

2.2、Iterator trait

2.2.1、Iterator trait

All iterators implement the Iterator trait, which is defined in the standard library. The definition is roughly as follows.

pub trait Iterator {
    
    
    type Item;

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

    // 省略其余有默认实现的方法
}

2.2.2, next method

Each time an item in the iterator is returned, the returned result is wrapped in Some, and None is returned at the end of the iteration.

fn main() {
    
    
    let arr = [1, 2, 3];
    let mut arr_iter = arr.into_iter();

    assert_eq!(arr_iter.next(), Some(1));
    assert_eq!(arr_iter.next(), Some(2));
    assert_eq!(arr_iter.next(), Some(3));
    assert_eq!(arr_iter.next(), None);
}

2.3. Iterative method

iter: create an iterator over an immutable reference
into_iter: the created iterator takes ownership
iter_mut: iterate over a mutable reference

2.4. The method of consuming iterators

2.4.1, sum method

The method that calls the next method is called a consuming adapter. For example: Sum method: Take ownership of the iterator.

fn main() {
    
    
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);

    // v1_iter 是借用了 v1,因此 v1 可以照常使用
    println!("{:?}",v1);

    // 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权
    // println!("{:?}",v1_iter);
}

2.4.2, collect method

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 iterator such as Iterator<Item=(ValueFromA, ValueFromB)>, here it is 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<K, V>. Similarly, the type must be explicitly declared here, and then the KV type inside the HashMap can be handed over to the compiler for derivation. In the end, the compiler will deduce HashMap<&str, i32>, which is absolutely correct!

2.5. Iterator Adapter

2.5.1、map

Since the consumer adapter consumes the iterator and returns a value. Then the iterator adapter, as the name suggests, will return a new iterator, which is the key to implementing chained method calls: v.iter().map().filter()….

Unlike consumer adapters, iterator adapters are lazy, meaning you need a consumer adapter to wrap up and finally convert the iterator to a concrete value:

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]);

2.5.2、zip

2.5.3、filter

2.5.4、enumerate

使用 enumerate 方法可以获取迭代时的索引。
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i,v) in v.iter().enumerate() {
    
    
    println!("第{}个值是{}",i,v)
}

2.6, custom iterator

自定义迭代器很简单,我们只需要实现 Iterator 特征 以及next 方法即可。实际上  Iterator 之中还有其他方法,其他方法都有默认实现,无需手动去实现。
impl Iterator for Counter {
    
    
    type Item = u32;

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

4. Performance comparison: loop VS iterator

Iterators are one of Rust's zero-cost abstractions, meaning the abstraction introduces no runtime overhead.

Summarize

That's all for today

Guess you like

Origin blog.csdn.net/fj_Author/article/details/132222779