четверг, 30 января 2020 г.

Замыкания в Rust.

Мы уже говорили про замыкания (closures) в статье Обратные вызовы и лямбда выражения в си++. Сегодня мы рассмотрим, как Rust работает с замыканиями. Сначала поговорим про функции высшего порядка (highorder function), это функции, которые могут принимать в качестве своих параметров другие функции (низшего порядка). Это удобно использовать для обратных вызовов (callbacks), когда мы вызываем функцию, которая должна в свою очередь вызвать другую функцию, переданную ей в качестве аргумента.
 
Мы знаем, что параметры функции всегда имеют определение типа, чтобы указать тип - функция, мы используем обобщенный тип (generic), с требованием, что он должен имплементировать функциональную характеристику Fn, FnMut или FnOnce и указать сигнатуру функции например, Fn(i32)->i32, где в скобках указан тип параметра или параметров через запятую, а после -> указывается тип возвращаемого значения. На первый взгляд все немного сложно, это из-за правил владения ресурсами. Функциональные характеристики описывают правила доступа к окружению из замыканий. Для функций это не требуется, так как они не имеют доступа к окружению. Fn обозначает многократный не мутирующий доступ к значению, например по не мутирующей ссылке (&). FnOnce обозначает один мутирующий или один не мутирующий доступ к значению, например по мутирующей или не мутирующей ссылке (& или &mut). FnMut обозначает один мутирующий доступ к значению, например по мутирующей ссылке (&mut) и неявно включает FnOnce. Тема, конечно, не простая, но мы не будем углубляться и поговорим немного о другом.
 
Передача функции в качестве параметра (аргумента) другой функции имеет некоторые недостатки. Например, часто определенное действие требуется только один раз, но мы должны завернуть его в функцию, чья задача быть востребованной многократно, ситуация усугубляется при необходимости множества одноразовых действий. Проблема знакомая и нам на помощь приходят безымянные функции, которые мы можем создавать прямо в месте вызова. Из тела такой функции было бы очень удобно, получить доступ к переменным, находящимся в том же пространстве имен, т. е. к окружению (outside variables) функции. Про управление доступом к окружению мы говорили выше. Такие функции есть, они называются замыкания и имеют специальный синтаксис, который должен соответствовать функциональному типу (сигнатуре функции). Например, |x:i32|{x} соответствует сигнатуре (i32)->i32. Как мы видим Rust может вывести возвращаемый тип и его можно не указывать или указать явно |x:i32|->i32{x}. В примере показаны различные варианты использования.

//trait bounds for closures - access to outside variables
//1.Fn - multiple immutable access to value, can borrow as & (immutable reference)
fn higher_order_1(f: impl Fn(u32) -> u32) -> i32 {
    let x = f(5);
    x as i32
}

//2.FnMut - one mutable access to value, can borrow as &mut (mutable reference), implicit FnOnce
fn higher_order_2<F>(mut f: F) -> i32 where F: FnMut(u32) -> i32 {
    let x = f(5);
    x as i32 //
}

//3.FnOnce - one mut or immut access to value, can borrow as & (immut ref) or &mut (mut ref)
fn higher_order_3<F: FnOnce(u32) -> u32>(f: F) -> i32 {
    let x = f(5);
    x as i32
}

//simple function
fn foo_1(x: u32) -> u32 {
    x + x
}

//can't use outside
fn foo_2(x: u32) -> i32 {
    //inside string
    let mut str_x = "x".to_string();
    str_x.push('X');
    println!("in the closure, str_x is now {}", str_x);
    (x + x) as i32
} //inside string deleted

fn main() {
    //outside string
    let mut str_y = "y".to_string();

    println!("higher_order_1: {:?}\n", higher_order_1(foo_1)); //Fn
    println!("higher_order_2: {:?}\n", higher_order_2(foo_2)); //FnMut

    //Closure
    println!("higher_order_3: {:?}", higher_order_3( //FnOnce
                                                     |x: u32| {
                                                         str_y.push('Y'); //change outside string
                                                         println!("in the closure, str_y is now {}", str_y);
                                                         x
                                                     }
    )); //outside string is alive
    println!("after higher_order_3, str_y is {}", str_y);
}

В стандартной библиотеке Rust имеется множество функций, которые принимают в качестве своих аргументов замыкания, например sort_by(), которая принимает замыкание с сигнатурой (&T,  &T) -> Ordering. Перечисление Ordering состоит из вариантов Greater, Less и Equal. Эта функция работает с коллекциями, принимая на вход два соседних элемента по очереди, сравнивает их и сортирует. В замыкании мы можем сами определять способ сравнения, если это обычное больше-меньше, то можно использовать стандартную для этой цели функцию cmp(). Замечу, что если замыкание состоит из одного выражения, то его тело можно не заключать в блок (фигурные скобки). Смотрим пример.

use std::cmp::Ordering;

fn cmp1(a: &i32, b: &i32) -> Ordering {
    if a < b {
        Ordering::Greater
    } else if a > b {
        Ordering::Less
    } else {
        Ordering::Equal
    }
}

fn cmp2(a: &i32, b: &i32) -> Ordering {
    a.cmp(b)
}

fn main() {
    let mut arr = [4, 8, 1, 10, 0, 45, 12, 7];
    arr.sort_by(cmp1);
    println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
    arr.sort_by(cmp2);
    println!("{:?}", arr); //[0, 1, 4, 7, 8, 10, 12, 45]
    arr.sort_by(|a, b| b.cmp(a));
    println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
}

Перегуд В.

Комментариев нет:

Отправить комментарий