Rust - Basic - 12 - Iterators and Closures

イテレーターとクロージャ

イテレーターとクロージャは、関数型プログラミングの考え方の一つです。

理解

  • クロージャ

    特徴: 関数を変数に代入でき、その時では関数は呼び出されません。

    特徴: 呼び出し側に公開されず、短く意味が明確です。

    コンパイラーは推論によって使する Closure の種類を決めます。

    FnOnce 部環境の有権を取得 (move) する場合は FnOnce を使います。

    FnMut 部環境を変 borrow する場合は FnMut を使います。

    Fn 部環境を不変 borrow する場合は Fn を使います。

    環境とは environment のことで、通常はクロージャで使う部変数を指します。

    // 常见声明
    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));
    }
    
  • イテレーター

    イテレーションとループの性に差はありません。

    // 基础写法
    #[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);
    }
    

参照元

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

…

クロージャの型推論と注釈

クロージャでは、fn 関数のようにパラメーターや戻り値の型を注釈する必要がありません。 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.

クロージャは通常、任意の状況ではなく、狭い文脈の中で 短く関連性の高い ものとして使われます。 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.

このような小さな匿名関数に型注釈を求めるのは煩雑で、コンパイラーがすでに持つ情報と大部分が重複します。

…

クロージャによる環境のキャプチャ

ワークアウト生成器の例では、クロージャをインライン匿名関数としてだけ使いました。 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.

リスト 13-12 には、クロージャを囲む環境の x を使うクロージャを equal_to_x 変数に保存する例があります。

ファイル名: src/main.rs

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

リスト 13-12: 囲むスコープの変数を参照するクロージャの例

ここでは x は equal_to_x のパラメーターではありませんが、equal_to_x と同じスコープで定義された x をクロージャが使えます。

関数では同じことはできません。次の例を試すとコードはコンパイルできません。

ファイル名: 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));
}

次のエラーが発生します。

$ 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

コンパイラーも、これがクロージャでのみ動作すると注意してくれます。

クロージャが環境から値をキャプチャすると、その値を本体で使うためにメモリへ保存します。 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.

クロージャが環境から値をキャプチャする方法は 3 つあり、関数が引数を受け取る 3 つの方法、つまり 有権の取得、変借、不変借 に対応します。 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 は値を変借するため、環境を変できます。
  • Fn は環境の値を不変借します。

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.

クロージャが環境で使う値の有権を取得するよう強制するには、パラメーターリストの前に move キーワードを置きます。 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.

ファイル名: 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));
}

次のエラーが発生します。

$ 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

move キーワードを追加したため、クロージャ定義時に x の値がクロージャへ move されます。 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.