Замыкания в Rust



Книга Замыкания в Rust

В Rust можно создавать не только именованные функции, но и анонимные, которые называются замыканиями. Сами по себе они не так уж интересны, пока вы не объединяете их с функциями, которые принимают замыкания в качестве аргументов. Вот где реальная мощь!


Давайте создадим замыкание:


let add_one = |x| { 1 + x };

println!("The sum of 5 plus 1 is {}.", add_one(5));

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


Давайте сравним синтаксис. Он практически идентичен:


let add_one = |x: i32| -> i32 { 1 + x };
fn add_one (x: i32) -> i32 { 1 + x }

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


Между замыканием и именованными функциями есть и ещё одно большое различие, оно обусловлено названием: замыкание «замыкает своё окружение», захватывая его переменные. Что это значит? Посмотрите:


fn main() {
let x: i32 = 5;

let printer = || { println!("x is: {}", x); };

printer(); // выводит «x is: 5»
}

Синтаксис || указывает на то, что это анонимное замыкание, которое не принимает никаких аргументов. Без него у нас был бы просто блок кода в фигурных скобках {}.


Другими словами, замыкание получает доступ к переменным из области видимости, в которой оно определено. Замыкание заимствует любые используемые им переменные, поэтому здесь у нас возникнет ошибка:


fn main() {
let mut x: i32 = 5;

let printer = || { println!("x is: {}", x); };

x = 6; // ошибка: не удаётся присвоить значение переменной «x», так как она заимствована
}

Перемещающие замыкания


В Rust есть и второй тип замыкания — перемещающее. На перемещающие замыкания указывает ключевое слово move (например, move || x * x). Разница между перемещающим замыканием и обычным заключается в том, что первое всегда забирает во владение все переменные, которые оно использует. Второе лишь создаёт ссылку на стековый фрейм, охватывающий её окружение. Перемещающие замыкания широко применяются в сочетании с функциональными средствами Rust, использующимися при одновременном выполнении нескольких задач.


Замыкания в качестве аргументов


Наибольший интерес замыкания представляют в роли аргумента для другой функции. Вот пример:


fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
f(x) + f(x)
}

fn main() {
let square = |x: i32| { x * x };

twice(5, square); // оказывается равным 50
}

Разберём его, начиная с main:


let square = |x: i32| { x * x };

Это мы уже видели в начале статьи. Создаём замыкание, которое принимает целочисленное значение и возвращает его квадрат.


twice(5, square); // оказывается равным 50

А эта строчка поинтереснее. Здесь мы вызываем функцию twice и передаем ей два аргумента: целое число 5 и замыкание square. Работает это точно так же, как передача в функцию любых других двух привязок переменных, но если вы никогда раньше с замыканиями дела не имели, то может показаться немного сложновато. Тогда просто представьте, что передаёте две переменные: одна переменная — это i32, а другая — функция.


Теперь посмотрим, как определяется twice:


fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {

twice принимает два аргумента: x и f. Потому-то мы и вызывали его с двумя аргументами. x — это i32, мы делали это много раз. А вот f — это функция, которая принимает i32 и возвращает i32. Именно на это указывает требование Fn(i32) -> i32 к параметру типа F. То есть F представляет собой любую функцию, которая принимает i32 и возвращает i32.


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


В итоге twice тоже возвращает i32.


Посмотрим теперь на тело функции twice:


fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
f(x) + f(x)
}

Замыкание называется f, поэтому мы можем вызывать его точно так же, как и предыдущие. Передаём аргумент x в каждое из них. Отсюда и название функции twice, что означает «дважды».


Выполнив вычисления, получим такой результат: (5 * 5) + (5 * 5) == 50.


Эту технику стоит освоить, поскольку стандартная библиотека Rust широко использует замыкания.


Название square можно и задавать, а просто сделать его значение подставляемым. Вот точно такой же пример, как и предыдущий:


fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
f(x) + f(x)
}

fn main() {
twice(5, |x: i32| { x * x }); // оказывается равным 50
}

Название именованной функции можно использовать везде, где применяется замыкание. Этот же пример можно записать по-другому:


fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
f(x) + f(x)
}

fn square(x: i32) -> i32 { x * x }

fn main() {
twice(5, square); // оказывается равным 50
}

Такое встречается нечасто, но время от времени можно делать и так.


И в заключение рассмотрим функцию, которая принимает два замыкания:


fn compose<F, G>(x: i32, f: F, g: G) -> i32
where F: Fn(i32) -> i32, G: Fn(i32) -> i32 {
g(f(x))
}

fn main() {
compose(5,
|n: i32| { n + 42 },
|n: i32| { n * 2 }); // оказывается равным 94
}

Вы можете задаться вопросом: зачем здесь два параметра типа F и G, ведь оба они имеют одну и ту же сигнатуру: Fn(i32) -> i32.


А всё потому, что в Rust у каждого замыкания свой уникальный тип. Мало того, что замыкания с разными сигнатурами имеют разные типы, так ещё и у замыканий с одной и той же сигнатурой тоже разные типы!


Для простоты можно считать, что поведение замыкания — это часть его типа. Поэтому при использовании одного параметра типа для обоих замыканий будет принято первое из них и отвергнуто второе. Уникальный тип второго замыкания не позволяет ему быть представленным тем же параметром типа, что и у первого. Поэтому мы и используем два разных типа параметров: F и G.


Здесь также появляется оператор where, дающий возможность более гибко описывать параметры типа.


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




559   0  

Comments

    Ничего не найдено.