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

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

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

Мы уже обсуждали, как передавать замыкания в функции; но также можно передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать ранее созданную функцию, а не определять новое замыкание. Функции соответствуют типу 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 для получения указателя функции в качестве аргумента

Этот код печатает The answer is: 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. Чтобы использовать функцию 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
  |
  = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~~~~~~~~~~~~~~~~

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

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

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

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

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