Источник: Nuances of Programming
В 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 }
Как вы могли заметить, замыкания выводят аргумент и возвращают типы, поэтому объявлять его не нужно. Это отличает их от именованных функций, которые по умолчанию принимают значения, возвращающиеся в круглых скобках ().
Между замыканием и именованными функциями есть и ещё одно большое различие, оно обусловлено названием: замыкание «замыкает своё окружение», захватывая его переменные. Что это значит? Посмотрите:
Синтаксис || указывает на то, что это анонимное замыкание, которое не принимает никаких аргументов. Без него у нас был бы просто блок кода в фигурных скобках {}.
Другими словами, замыкание получает доступ к переменным из области видимости, в которой оно определено. Замыкание заимствует любые используемые им переменные, поэтому здесь у нас возникнет ошибка:
Перемещающие замыкания
В Rust есть и второй тип замыкания — перемещающее. На перемещающие замыкания указывает ключевое слово move (например, move || x * x). Разница между перемещающим замыканием и обычным заключается в том, что первое всегда забирает во владение все переменные, которые оно использует. Второе лишь создаёт ссылку на стековый фрейм, охватывающий её окружение. Перемещающие замыкания широко применяются в сочетании с функциональными средствами Rust, использующимися при одновременном выполнении нескольких задач.
Замыкания в качестве аргументов
Наибольший интерес замыкания представляют в роли аргумента для другой функции. Вот пример:
Разберём его, начиная с 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 можно и задавать, а просто сделать его значение подставляемым. Вот точно такой же пример, как и предыдущий:
Название именованной функции можно использовать везде, где применяется замыкание. Этот же пример можно записать по-другому:
Такое встречается нечасто, но время от времени можно делать и так.
И в заключение рассмотрим функцию, которая принимает два замыкания:
Вы можете задаться вопросом: зачем здесь два параметра типа F и G, ведь оба они имеют одну и ту же сигнатуру: Fn(i32) -> i32.
А всё потому, что в Rust у каждого замыкания свой уникальный тип. Мало того, что замыкания с разными сигнатурами имеют разные типы, так ещё и у замыканий с одной и той же сигнатурой тоже разные типы!
Для простоты можно считать, что поведение замыкания — это часть его типа. Поэтому при использовании одного параметра типа для обоих замыканий будет принято первое из них и отвергнуто второе. Уникальный тип второго замыкания не позволяет ему быть представленным тем же параметром типа, что и у первого. Поэтому мы и используем два разных типа параметров: F и G.
Здесь также появляется оператор where, дающий возможность более гибко описывать параметры типа.
Вот и всё, что нужно знать, чтобы освоить замыкания! На первый взгляд они кажутся немного странными, но стоит только к ним привыкнуть, как вам будет недоставать их в других языках. Передача функций другим функциям обладает невероятной мощью, убедитесь сами!
Читайте также:
Перевод статьи Omar Faroque: Best explanation of closure in Rust