Rust - Basic - 12 - Iterators and Closures

Iterators and Closures

迭代器与闭包,函数式编程的思想之一

Comprehension

  • closures

    表现:函数可以被赋值为变量,此时函数未被调用

    特点:不对调用者暴露、简短且意义明确

    编译器会进行推断以决定使用哪种 Closures:

    FnOnce 当使用的外部环境被 taking ownership (move) 则使用

    FnMut 当使用的外部环境被 borrow mutably 则使用

    Fn 当使用的外部环境被 borrow immutably 则使用

    环境即 envrionment,一般指闭包内使用的外部变量

    // 常见声明
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };
    
    // 只有 a single expression 的时候
    let add_one_v3 = |x|             { x + 1 };
    // 大括号可以去掉
    let add_one_v4 = |x|               x + 1  ;
    
    // closures 自带 type inferred
    let example_closure = |x| x;
    let s = example_closure(String::from("hello"));
    let n = example_closure(5);  // 编译器处理到这里的时候会报错
    
    // 使用 struct + closures + trait 实现 lazy load
    struct Cacher<T>
    where
        T: Fn(u32) -> u32,
    {
        calculation: T,
        value: Option<u32>,
    }
    impl<T> Cacher<T>
    where
        T: Fn(u32) -> u32,
    {
        fn new(calculation: T) -> Cacher<T> {
            Cacher {
                calculation,
                value: None,
            }
        }
    
        fn value(&mut self, arg: u32) -> u32 {
            match self.value {
                Some(v) => v,
                None => {
                    let v = (self.calculation)(arg);
                    self.value = Some(v);
                    v
                }
            }
        }
    }
    
    // 通过 move 关键字强制指定
    // 一般不会这么做(交给编译器推导)
    // 以下代码无法通过编译(因为 x 被 move 了)
    fn main() {
        let x = vec![1, 2, 3];
    
        let equal_to_x = move |z| z == x;
    
        println!("can't use x here: {:?}", x);
    
        let y = vec![1, 2, 3];
    
        assert!(equal_to_x(y));
    }
    
  • iterators

    迭代 和 循环 性能没有差别。

    // 基础写法
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];
    		// 由于后续需要 next,会修改 v1 迭代器状态,故需要 mut 关键字
        // 如果需要 take ownership,则使用 into_iter
    		// 如果需要 mutable referance,则使用 iter_mut
        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);
    }
    
    let v1: Vec<i32> = vec![1, 2, 3];
    // 这里没有 consuming adaptors(调用 next 的 methods)会触发 compile warning
    v1.iter().map(|x| x + 1);
    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // 使用 collect 消费迭代器
    assert_eq!(v2, vec![2, 3, 4]);
    
    /// into_inter 使用示例 - filter
    #[derive(PartialEq, Debug)]
    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()
    }
    
    // 实现一个能 iter 的 struct
    struct Counter {
        count: u32,
    }
    impl Counter {
        fn new() -> Counter {
            Counter { count: 0 }
        }
    }
    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
            }
        }
    }
    #[test]
    fn calling_next_directly() {
        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);
    }
    
    // 使用其它 Iterator Trait Methods
    #[test]
    fn using_other_iterator_trait_methods() {
        let sum: u32 = Counter::new()
            .zip(Counter::new().skip(1))  // 任一 iter 返回 None,zip 停止 pair items,所以这里只有 4 pairs
            .map(|(a, b)| a * b)
            .filter(|x| x % 3 == 0)
            .sum();
        assert_eq!(18, sum);
    }
    

Origin

https://doc.rust-lang.org/book/ch13-00-functional-features.html

Closure Type Inference and Annotation

Closures don’t require you to annotate the types of the parameters or the return value like fn functions do. Type annotations are required on functions because they’re part of an explicit interface exposed to your users. Defining this interface rigidly is important for ensuring that everyone agrees on what types of values a function uses and returns. But closures aren’t used in an exposed interface like this: they’re stored in variables and used without naming them and exposing them to users of our library.

Closures are usually short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler is reliably able to infer the types of the parameters and the return type, similar to how it’s able to infer the types of most variables.

Making programmers annotate the types in these small, anonymous functions would be annoying and largely redundant with the information the compiler already has available.

Capturing the Environment with Closures

In the workout generator example, we only used closures as inline anonymous functions. However, closures have an additional capability that functions don’t have: they can capture their environment and access variables from the scope in which they’re defined.

Listing 13-12 has an example of a closure stored in the equal_to_x variable that uses the x variable from the closure’s surrounding environment.

Filename: src/main.rs

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

Listing 13-12: Example of a closure that refers to a variable in its enclosing scope

Here, even though x is not one of the parameters of equal_to_x, the equal_to_x closure is allowed to use the x variable that’s defined in the same scope that equal_to_x is defined in.

We can’t do the same with functions; if we try with the following example, our code won’t compile:

Filename: src/main.rs

fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool {
        z == x
    }

    let y = 4;

    assert!(equal_to_x(y));
}

We get an error:

$ cargo run
   Compiling equal-to-x v0.1.0 (file:///projects/equal-to-x)
error[E0434]: can't capture dynamic environment in a fn item
 --> src/main.rs:5:14
  |
5 |         z == x
  |              ^
  |
  = help: use the `|| { ... }` closure form instead

For more information about this error, try `rustc --explain E0434`.
error: could not compile `equal-to-x` due to previous error

The compiler even reminds us that this only works with closures!

When a closure captures a value from its environment, it uses memory to store the values for use in the closure body. This use of memory is overhead that we don’t want to pay in more common cases where we want to execute code that doesn’t capture its environment. Because functions are never allowed to capture their environment, defining and using functions will never incur this overhead.

Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing mutably, and borrowing immutably. These are encoded in the three Fn traits as follows:

  • FnOnce consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.
  • FnMut can change the environment because it mutably borrows values.
  • Fn borrows values from the environment immutably.

When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement FnOnce because they can all be called at least once. Closures that don’t move the captured variables also implement FnMut, and closures that don’t need mutable access to the captured variables also implement Fn. In Listing 13-12, the equal_to_x closure borrows x immutably (so equal_to_x has the Fn trait) because the body of the closure only needs to read the value in x.

When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement FnOnce because they can all be called at least once. Closures that don’t move the captured variables also implement FnMut, and closures that don’t need mutable access to the captured variables also implement Fn. In Listing 13-12, the equal_to_x closure borrows x immutably (so equal_to_x has the Fn trait) because the body of the closure only needs to read the value in x.

If you want to force the closure to take ownership of the values it uses in the environment, you can use the move keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread.

Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them. The move keyword only specifies the latter.

We’ll have more examples of move closures in Chapter 16 when we talk about concurrency. For now, here’s the code from Listing 13-12 with the move keyword added to the closure definition and using vectors instead of integers, because integers can be copied rather than moved; note that this code will not yet compile.

Filename: src/main.rs

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

    let equal_to_x = move |z| z == x;

    println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

We receive the following error:

$ cargo run
   Compiling equal-to-x v0.1.0 (file:///projects/equal-to-x)
error[E0382]: borrow of moved value: `x`
 --> src/main.rs:6:40
  |
2 |     let x = vec![1, 2, 3];
  |         - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |
4 |     let equal_to_x = move |z| z == x;
  |                      --------      - variable moved due to use in closure
  |                      |
  |                      value moved into closure here
5 |
6 |     println!("can't use x here: {:?}", x);
  |                                        ^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `equal-to-x` due to previous error

The x value is moved into the closure when the closure is defined, because we added the move keyword. The closure then has ownership of x, and main isn’t allowed to use x anymore in the println! statement. Removing println! will fix this example.

Most of the time when specifying one of the Fn trait bounds, you can start with Fn and the compiler will tell you if you need FnMut or FnOnce based on what happens in the closure body.