В 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, дающий возможность более гибко описывать параметры типа.
Вот и всё, что нужно знать, чтобы освоить замыкания! На первый взгляд они кажутся немного странными, но стоит только к ним привыкнуть, как вам будет недоставать их в других языках. Передача функций другим функциям обладает невероятной мощью, убедитесь сами!
Читайте также:
- Диагностика кода на Rust
- Как спроектировать REST API для выполнения системных команд с помощью Actix Rust
- Rust: работа с потоками
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Omar Faroque: Best explanation of closure in Rust




