Rust - Basic - 12 - Итераторы и замыкания

Итераторы и замыкания

Итераторы и замыкания — одна из идей функционального программирования

Кратко

  • 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 выводят тип автоматически
    let example_closure = |x| x;
    let s = example_closure(String::from("hello"));
    let n = example_closure(5);  // компилятор сообщит об ошибке, когда дойдёт до этого места
    
    // ленивая загрузка (lazy load) через struct + closures + trait
    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 перемещён)
    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 reference, используйте 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 (method, вызывающих next), поэтому будет 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_iter — 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()
    }
    
    // реализуем 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 перестаёт соединять пары, поэтому здесь только 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. Аннотации типов обязательны для функций, потому что они часть явного интерфейса, раскрываемого пользователям. Жёстко задавать этот интерфейс важно, чтобы все соглашались, какие типы значений функция принимает и возвращает. Но замыкания не используются в таком раскрываемом интерфейсе: они хранятся в переменных и используются без имени, не раскрываясь пользователям нашей библиотеки.

Замыкания обычно короткие и уместные только в узком контексте, а не в произвольном сценарии. В этих ограниченных контекстах компилятор надёжно выводит типы параметров и возвращаемый тип — так же, как он выводит типы большинства переменных.

Заставлять программистов аннотировать типы в таких маленьких анонимных функциях было бы утомительно и в основном избыточно: компилятор уже располагает этой информацией.

…

Захват окружения замыканиями

В примере с генератором тренировок мы использовали замыкания только как встроенные анонимные функции. Однако у замыканий есть дополнительная возможность, которой нет у функций: они могут захватывать своё окружение и обращаться к переменным из области видимости, в которой определены.

В листинге 13-12 приведён пример замыкания, сохранённого в переменной equal_to_x, которое использует переменную 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, определённую в той же области видимости, где определён equal_to_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

Компилятор даже напоминает, что это работает только с замыканиями!

Когда замыкание захватывает значение из окружения, оно использует память для хранения значений, нужных в теле замыкания. Это накладные расходы по памяти, которых мы не хотим платить в более частых случаях, когда нужно выполнить код, не захватывающий окружение. Поскольку функции никогда не могут захватывать своё окружение, определение и использование функций таких расходов не даёт.

Замыкания могут захватывать значения из окружения тремя способами, которые напрямую соответствуют трём способам, которыми функция может принимать параметр: taking ownership, заимствование как изменяемое и заимствование как неизменяемое. Это закодировано в трёх трейтах Fn следующим образом:

  • FnOnce потребляет переменные, захваченные из объемлющей области видимости, которая называется environment замыкания. Чтобы потребить захваченные переменные, замыкание должно взять их во владение и переместить в себя при определении. Часть Once в названии отражает тот факт, что замыкание не может взять во владение одни и те же переменные более одного раза, поэтому его можно вызвать только один раз.
  • FnMut может менять окружение, потому что заимствует значения как изменяемые.
  • Fn заимствует значения из окружения как неизменяемые.

Когда вы создаёте замыкание, Rust выводит, какой трейт использовать, исходя из того, как замыкание использует значения из окружения. Все замыкания реализуют FnOnce, потому что любое из них можно вызвать хотя бы один раз. Замыкания, которые не перемещают захваченные переменные, реализуют также FnMut, а замыкания, которым не нужен изменяемый доступ к захваченным переменным, реализуют также Fn. В листинге 13-12 замыкание equal_to_x заимствует x как неизменяемое (поэтому equal_to_x имеет трейт Fn), потому что телу замыкания нужно только прочитать значение x.

Когда вы создаёте замыкание, Rust выводит, какой трейт использовать, исходя из того, как замыкание использует значения из окружения. Все замыкания реализуют FnOnce, потому что любое из них можно вызвать хотя бы один раз. Замыкания, которые не перемещают захваченные переменные, реализуют также FnMut, а замыкания, которым не нужен изменяемый доступ к захваченным переменным, реализуют также Fn. В листинге 13-12 замыкание equal_to_x заимствует x как неизменяемое (поэтому equal_to_x имеет трейт Fn), потому что телу замыкания нужно только прочитать значение x.

Если нужно заставить замыкание взять во владение значения, которые оно использует из окружения, можно поставить ключевое слово move перед списком параметров. Этот приём в основном полезен при передаче замыкания в новый thread, чтобы переместить данные и сделать их собственностью нового thread.

Замечание: замыкания move всё равно могут реализовывать Fn или FnMut, хотя захватывают переменные через move. Это потому, что реализуемые замыканием трейты определяются тем, что замыкание делает с захваченными значениями, а не тем, как оно их захватывает. Ключевое слово move определяет только второе.

Больше примеров замыканий move будет в главе 16, где речь пойдёт о многопоточности. А пока вот код из листинга 13-12 с ключевым словом move в определении замыкания и с векторами вместо целых чисел, потому что целые числа копируются, а не перемещаются; заметьте, что этот код пока не скомпилируется.

Имя файла: 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

Значение x перемещается в замыкание при его определении, потому что мы добавили ключевое слово move. После этого замыкание владеет x, и main больше не может использовать x в операторе println!. Удаление println! исправит этот пример.

В большинстве случаев, указывая одно из trait bounds Fn, можно начать с Fn: компилятор сам скажет, нужны ли FnMut или FnOnce, исходя из того, что происходит в теле замыкания.