Продвинутые функции и замыкания

В этом разделе рассматриваются некоторые продвинутые возможности, относящиеся к функциям и замыканиям, такие как указатели функций и возвращаемые замыкания.

Указатели функций

Мы уже обсуждали, как передавать замыкания в функции; но также можно передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать ранее созданную функцию, а не определять новое замыкание. Функции соответствуют типу fn (со строчной буквой f), не путать с трейтом замыкания Fn. Тип fn называется указателем функции. Передача функций с помощью указателей функций позволяет использовать функции в качестве аргументов других функций.

Для указания того, что параметр является указателем на функцию, используется синтаксис, такой же, как и для замыканий, что демонстрируется в листинге 19-27, где мы определили функцию add_one, которая добавляет единицу к переданному ей параметру. Функция do_twice принимает два параметра: указатель на любую функцию, принимающую параметр i32 и возвращающую i32, и число типа i32. Функция do_twice дважды вызывает функцию f, передавая ей значение arg, а затем складывает полученные результаты. Функция main вызывает функцию do_twice с аргументами add_one и 5.

Файл: src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}

Листинг 19-27: Использование типа fn для получения указателя на функцию в качестве аргумента

Этот код выводит Ответ: 12. Мы указали, что параметр f в do_twice является fn, которая принимает на вход единственный параметр типа i32 и возвращает i32. Затем мы можем вызвать f в теле do_twice. В main мы можем передать имя функции add_one в качестве первого аргумента в do_twice.

В отличие от замыканий, fn является типом, а не трейтом, поэтому мы указываем fn непосредственно в качестве типа параметра, а не объявляем параметр универсального типа с одним из трейтов Fn в качестве связанного.

Указатели функций реализуют все три трейта замыканий (Fn, FnMut и FnOnce), то есть вы всегда можете передать указатель функции в качестве аргумента функции, которая ожидает замыкание. Лучше всего для описания функции использовать универсальный тип и один из трейтов замыканий, чтобы ваши функции могли принимать как функции, так и замыкания.

Однако, одним из примеров, когда вы бы хотели принимать только fn, но не замыкания, является взаимодействие с внешним кодом, который не имеет замыканий: функции языка C могут принимать функции в качестве аргументов, однако замыканий в языке C нет.

В качестве примера того, где можно использовать либо замыкание, определяемое непосредственно в месте передачи, либо именованную функцию, рассмотрим использование метода map, предоставляемого трейтом Iterator в стандартной библиотеке. Чтобы использовать функцию map для преобразования вектора чисел в вектор строк, мы можем использовать замыкание, например, так:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

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

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Продвинутые трейты", потому что доступно несколько функций с именем to_string. Здесь мы используем функцию to_string определённую в типаже ToString, который реализован в стандартной библиотеке для любого типа реализующего типаж Display.

Вспомните из раздела "Значения перечислений" главы 6, что имя каждого определённого нами варианта перечисления также становится функцией-инициализатором. Мы можем использовать эти инициализаторы в качестве указателей на функции, реализующих трейты замыканий, что означает, что мы можем использовать инициализаторы в качестве аргументов для методов, принимающих замыкания, например, так:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

Здесь мы создаём экземпляры Status::Value, используя каждое значение u32 в диапазоне (0..20), с которым вызывается map с помощью функции инициализатора Status::Value. Некоторые люди предпочитают этот стиль, а некоторые предпочитают использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее.

Возврат замыканий

Замыкания представлены трейтами, что означает, что вы не можете возвращать замыкания из функций. В большинстве случаев, когда вам захочется вернуть трейт, вы можете использовать конкретный тип, реализующий этот трейт, в качестве возвращаемого значения функции. Однако вы не можете сделать подобного с замыканиями, поскольку у них не может быть конкретного типа, который можно было бы вернуть; например, вы не можете использовать указатель на функцию fn в качестве возвращаемого типа.

Следующий код пытается напрямую вернуть замыкание, но он не компилируется:

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

Ошибка компилятора выглядит следующим образом:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
help: return an `impl Trait` instead of a `dyn Trait`, if all returned values are the same type
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~
help: box the return type, and wrap all of the returned values in `Box::new`
  |
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~     Box::new(|x| x + 1)
  |

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error

Ошибка снова ссылается на типаж Sized ! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов" главы 17.

Далее давайте посмотрим на макросы!