В 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, дающий возможность более гибко описывать параметры типа.

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Omar Faroque: Best explanation of closure in Rust

Предыдущая статьяДоходчиво об обучении на основе многообразий с алгоритмами IsoMap, t-SNE и LLE
Следующая статьяРазвертывание Flask приложения на Heroku и подключение к БД MySQL  -  JawsDB