Rust - Basic - 12 - 반복자와 클로저

반복자와 클로저

반복자와 클로저는 함수형 프로그래밍의 핵심 개념 중 하나입니다.

이해

  • 클로저 표현: 함수를 변수에 할당할 수 있으며 이때 함수는 호출되지 않습니다. 특징: 호출자에게 노출되지 않고 짧으며 의미가 명확합니다. 컴파일러가 추론해 어떤 클로저를 사용할지 결정합니다. FnOnce 외부 환경의 소유권을 가져오면(taking ownership, move) 사용합니다. FnMut 외부 환경을 가변으로 borrow하면 사용합니다. Fn 외부 환경을 불변으로 borrow하면 사용합니다. 환경(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 함수와 달리 매개변수나 반환값의 형식에 주석을 달 필요가 없습니다. 함수에는 사용자에게 공개되는 명시적 인터페이스의 일부이므로 형식 주석이 필요합니다. 이 인터페이스를 엄격하게 정의해야 함수가 사용하고 반환하는 값의 형식에 모두가 동의할 수 있습니다. 하지만 클로저는 이렇게 공개된 인터페이스에서 사용되지 않습니다. 클로저는 변수에 저장되어 이름을 붙이거나 라이브러리 사용자에게 공개하지 않고 사용됩니다. 클로저는 임의의 상황에서 사용되기보다 보통 좁은 문맥 안에서만 짧고 적절하게 사용됩니다. 이렇게 제한된 문맥에서는 컴파일러가 대부분의 변수 형식을 추론하는 것처럼 매개변수와 반환값의 형식도 안정적으로 추론할 수 있습니다. 이처럼 작고 익명인 함수에 형식 주석을 달도록 프로그래머에게 요구하는 것은 번거롭고, 컴파일러가 이미 가진 정보와 대부분 중복됩니다. …

클로저로 환경 캡처하기

운동 생성기 예제에서는 클로저를 인라인 익명 함수로만 사용했습니다. 하지만 클로저에는 함수에 없는 능력이 하나 더 있습니다. 자신의 환경을 캡처하고 정의된 scope의 변수에 접근할 수 있습니다. 목록 13-12에는 equal_to_x 변수에 저장된 클로저의 예가 있습니다. 이 클로저는 주변 환경의 x 변수를 사용합니다. Filename: 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: 바깥 scope의 변수를 참조하는 클로저 예시 여기서 x는 equal_to_x의 매개변수가 아니지만, equal_to_x가 정의된 동일한 scope에 정의된 x 변수를 클로저가 사용할 수 있습니다. 함수로는 같은 일을 할 수 없습니다. 다음 예제를 시도하면 코드가 컴파일되지 않습니다: 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));
}

다음 오류가 발생합니다:

$ 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

컴파일러도 이것이 클로저에서만 작동한다고 알려 줍니다! 클로저가 환경의 값을 캡처하면 클로저 본문에서 사용하기 위해 값을 메모리에 저장합니다. 환경을 캡처하지 않는 코드를 실행하려는 일반적인 경우에는 이런 메모리 사용이 오버헤드가 됩니다. 함수는 환경을 캡처할 수 없으므로 함수를 정의하고 사용해도 이 오버헤드가 발생하지 않습니다. 클로저는 환경의 값을 세 가지 방식으로 캡처할 수 있습니다. 이는 함수가 매개변수를 받는 세 방식인 소유권 가져오기, 가변 borrow, 불변 borrow에 직접 대응합니다. 이 방식은 세 Fn trait에 다음과 같이 인코딩됩니다:

  • 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. 클로저를 만들면 Rust는 클로저가 환경의 값을 사용하는 방식에 따라 사용할 trait을 추론합니다. 모든 클로저는 최소 한 번 호출될 수 있으므로 FnOnce를 구현합니다. 캡처한 변수를 move하지 않는 클로저는 FnMut도 구현하며, 캡처한 변수에 가변 접근이 필요하지 않은 클로저는 Fn도 구현합니다. 목록 13-12에서 equal_to_x 클로저는 x의 값을 읽기만 하므로 x를 불변으로 borrow하고(equal_to_x가 Fn trait을 가짐) 있습니다. 클로저를 만들면 Rust는 클로저가 환경의 값을 사용하는 방식에 따라 사용할 trait을 추론합니다. 모든 클로저는 최소 한 번 호출될 수 있으므로 FnOnce를 구현합니다. 캡처한 변수를 move하지 않는 클로저는 FnMut도 구현하며, 캡처한 변수에 가변 접근이 필요하지 않은 클로저는 Fn도 구현합니다. 목록 13-12에서 equal_to_x 클로저는 x의 값을 읽기만 하므로 x를 불변으로 borrow하고(equal_to_x가 Fn trait을 가짐) 있습니다. 클로저가 환경에서 사용하는 값의 소유권을 가져오도록 강제하려면 매개변수 목록 앞에 move 키워드를 사용할 수 있습니다. 이 기법은 클로저를 새 스레드에 전달해 데이터가 새 스레드의 소유가 되도록 할 때 주로 유용합니다.

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.

동시성에 대해 다루는 16장에서 move 클로저의 예를 더 살펴봅니다. 여기서는 목록 13-12의 클로저 정의에 move 키워드를 추가하고, 정수는 move 대신 복사할 수 있으므로 정수 대신 벡터를 사용합니다. 이 코드는 아직 컴파일되지 않는다는 점에 유의하세요. 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));
}

다음 오류가 발생합니다:

$ 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 값이 클로저로 이동합니다. 그러면 클로저가 x의 소유권을 가지므로 main의 println! 문에서는 더 이상 x를 사용할 수 없습니다. println!을 제거하면 이 예제가 수정됩니다. Fn trait bound 중 하나를 지정할 때는 대부분 Fn으로 시작하면 됩니다. 클로저 본문에서 일어나는 일에 따라 FnMut 또는 FnOnce가 필요한 경우 컴파일러가 알려 줍니다.