Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Анонимные функции

Rust испытал на себе определённое влияние функционального программирования, поэтому имеет поддержку анонимных функций, замыканий и функций высшего порядка.

Анонимные функции

Анонимная функция — функция, являющаяся объектом, который можно присвоить переменной, передать в качестве аргумента в другую функцию и, разумеется, вызвать.

Анонимные функции декларируются при помощи следующего синтаксиса:

|аргумент_1, …, аргумент_n| -> ТипРезультата { тело функции }

Tip

Исходный код анонимной функции часто называют “функциональным литералом”.

Например:

fn main() {
    // создаём анонимную функцию, и присваиваем её переменной
    let inc: fn(i32) -> i32 = |x: i32| { x + 1 };
    let a = 1;
    // вызываем нашу функцию абсолютно так же, как и обычную
    let b = inc(a);
    println!("{b}"); // 2
}

Обратите внимание, что тип анонимной функции имеет вид:

fn(тип_аргумента_1, ..., тип_аргумента_2) -> тип_результата

Такой тип называется указателем на функцию (function pointer).

Как и в случае с обычными переменными, тип анонимной функции можно не указывать, если компилятор в состоянии вывести его по телу анонимной функции.

#![allow(unused)]
fn main() {
let inc = |x: i32| { x + 1 };
}

Также, фигурные скобки вокруг тела анонимной функции не обязательны, если оно состоит только из одного выражения:

#![allow(unused)]
fn main() {
let inc = |x: i32| x + 1;
}

В некоторых ситуациях компилятор способен вывести и типы аргументов. В этом случае можно опустить и типы.

fn main () {
    // Компилятор вывел тип x исходя из использования inc ниже.
    let inc = |x| x + 1;

    let a = 1;
    let b = inc(a);
    println!("{b}")
}

Note

Часто анонимные функции называют лямбда-выражениями, отсылаясь к разделу математики “Лямбда-исчисление”, которое легло в основу функционального программирования. Также анонимные функции часто называют лямбда-функциями.

Функции высшего порядка

Функцией высшего порядка называют функции, которые принимают другие функции в качестве аргумента либо возвращают функцию в качестве результата.

Для примера напишем функцию высшего порядка transform, которая принимает два аргумента — число и функцию, и возвращает результат применения этой функции к числу:

fn transform(a: i32, f: fn(i32) -> i32) -> i32 {
    f(a)
}

fn main() {
    let inc: fn(i32) -> i32 = |x: i32| { x + 1 };
    let a = 9;
    let b = transform(a, inc);
    println!("{b}"); // 10
}

Теперь рассмотрим пример, когда функция возвращает анонимную функцию в качестве результата:

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

fn main() {
    let inc = create_inc();
    let a = 1;
    let b = inc(a);
    println!("{b}"); // 2
}

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

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

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

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

fn main() {
    let inc: fn(i32) -> i32 = func_inc;
    let a = inc(7);
    println!("{a}"); // 8
}

Как мы видим, анонимные функции — это просто другой синтаксис для создания функций.

Замыкание

В примере выше мы сделали функцию create_inc, которая возвращает анонимную функцию — инкремент:

#![allow(unused)]
fn main() {
fn create_inc() -> fn(i32) -> i32 {
    |x: i32| x + 1 
}
}

Теперь давайте попробуем написать функцию, которая возвращает анонимную функцию, которая увеличивает свой аргумент не на единицу, а на заданный шаг:

#![allow(unused)]
fn main() {
fn make_inc_with_step(step: i32) -> fn(i32) -> i32 {
	|x| { x + step }
}
}

Увы, такой код не скомпилируется, и выдаст ошибку:

|x| { x + step }
^^^^^^^^^^^^^^^^ expected fn pointer, found closure

Дело в том, что из тела нашей анонимной функции, мы обращаемся к данным, которые принадлежат скоупу за пределами функции. Такая анонимная функция называется замыканием (closure).

Note

Такое название происходит из того, что анонимная функция как бы “замкнута” на свой внешний скоуп. Также часто говорят, что замыкание “захватывает” данные из внешнего контекста.

Замыкания, по своей природе, гораздо сложнее “чистых” анонимных функций (которые зависят только от своих аргументов). Чистая анонимная функция просто превращается в обычную функцию в сегменте кода, и мы работаем с ней через указатель на функцию. Замыкание же — это объект, представляющий из себя сложную комбинацию кода и захваченных данных.

Note

Понятие “чистой” функции широко используется в функциональном программировании. Чистая функция обращается исключительно к своим аргументам, и никак не взаимодействует с внешними по отношению к ней данными.

Давайте сначала перепишем код так, чтобы он работал, а дальше будем разбираться с устройством замыканий.

fn make_inc_with_step(step: i32) -> impl Fn(i32) -> i32 {
    move |x| { x + step }
}

fn main() {
    let inc_with_5 = make_inc_with_step(5);
    let a = inc_with_5(2);
    println!("{a}"); // 7
}

Как видим, у нас появилось два отличия:

  • fn(i32)->i32 превратилось в impl Fn(i32)->i32, что явно указывает на то, что замыкание — это не просто указатель на функцию, а объект некого типа, который реализует трэйт Fn.

  • Перед объявлением функционального литерала появилось ключевое слово move, которым мы явно указываем для компилятора, что если мы внутри замыкания используем какое-то значение из внешнего контекста, то владение над этим значением перемещается к замыканию.

Типы замыканий

Как мы уже сказали, для всех “чистых” анонимных функций, используется один тип данных — указатель на функцию, который имеет вид fn(...)->.... Вернее, сами типы отличаются, но все они объединены в семейство указателей на функции. Для замыканий же есть три разных трэйта:

  • Fn — трэйт для замыканий, которые захватывают значения из внешнего контекста по значению либо по немутабельной ссылке. Такие замыкания безопасно использовать в многопоточной среде, так как они только читают захваченное значение.

  • FnMut — трэйт для замыканий, которые захватывают значения из внешнего контекста и изменяют их по мутабельной ссылке. Такие замыкания нельзя использовать в многопоточной среде без предварительной синхронизации, так как они могут менять захваченное значение.

  • FnOnce — трэйт для замыканий, которые захватывают значения из внешнего контекста по значению (т.е. берут над ним владение) и уничтожают это значение в процессе своего вызова. Такое замыкание можно вызвать только один раз, так как после первого вызова значение будет уничтожено и недоступно для второго вызова.

Захват по ссылке и перемещение владения

Как мы сказали выше, замыкание может захватывать значения из внешнего контекста по значению или по ссылке. От того, каким образом захваченное значение будет использовано в теле замыкания, будет зависеть то, на основе какого трэйта компилятор сгенерирует тип замыкания: Fn, FnMut или FnOnce.

Рассмотрим самый простой пример захвата по немутабельной ссылке.

fn main() {
    let salutation = "Hello".to_string();

    // Тип замыкания: impl Fn(&str)->String
    let greet = |name: &str| make_greeting(&salutation, name);

    println!("{}", greet("John")); // Hello John

    // Переменная salutation захвачена замыканием по немутабельной ссылке,
    // поэтому всё еще может быть использована после захвата.
    print_string(salutation); // OK, data is still usable
}

fn make_greeting(salutation: &str, name: &str) -> String {
    format!("{} {}", &salutation, name)
}

fn print_string(s: String) {
    println!("{s}")
}

Захват значения переменной salutation произошёл по немутабельной ссылке, потому что в теле самого замыкания мы используем немутабельную ссылку &salutation .

Note

Нам пришлось создать отдельные функции make_greeting и print_string, чтобы по их сигнатурам было явно видно, какие аргументы используются по ссылке, а какие по значению. Использование макросов format! и println! напрямую нарушило бы чистоту эксперимента, так как эти макросы используют объекты строк по ссылке, даже если их явно передать по значению.

Если же в теле замыкания, мы используем по значению переменную, захваченную из внешнего контекста, то и захват произойдёт по значению.

fn main() {
    let salutation = "Hello".to_string();

    // Тип замыкания: impl FnOnce(&str)->String
    let greet = |name: &str| make_greeting(salutation, name);

    println!("{}", greet("John")); // Hello John

    // Теперь когда salutation захвачена по значению,
    // т.е. перемещена в замыкание использовать её нельзя
    print_string(salutation); // Error: use of moved value: `salutation`
}

fn make_greeting(salutation: String, name: &str) -> String {
    format!("{} {}", &salutation, name)
}

fn print_string(s: String) {
    println!("{s}")
}

Почему в предыдущих примерах перед анонимными функциями не использовалось ключевое слово move, как это было в примере из секции Замыкание? Дело в том, что move нужно явно указывать только, если замыкание живёт дольше скоупа, в котором оно создано. В нашем простом примере компилятор способен сам однозначно разобраться, какое поведение ожидается. Но если мы попытаемся вынести создание замыкания greet в отдельную функцию, то move понадобится:

#![allow(unused)]
fn main() {
fn make_greet_closure() -> impl Fn(&str) -> String {
    let salutation = "Hello".to_string();
    move |name: &str| make_greeting(&salutation, name)
}
}

Здесь наше замыкание живёт дольше, чем скоуп, в котором оно создано: скоуп (тело функции make_greet_closure) завершается, а замыкание продолжает жить в коде, который вызвал функцию make_greet_closure.

FnMut

Как мы уже сказали, если замыкание захватывает значение по мутабельной ссылке, то компилятор делает такое замыкание реализацией трэйта FnMut.

Для примера напишем еще одно инкрементирующее замыкание. Однако теперь это замыкание будет иметь ссылку на число, которое хранит шаг инкремента. Причём после каждого вызова этот шаг инкремента будет увеличиваться на единицу.

fn main() {
    let mut step = 1;

    // impl FnMut(i32)->i32
    let mut growing_inc = |x: i32| {
        let step_ref = &mut step;
        let res = x + *step_ref;
        *step_ref += 1;
        res
    };
    println!("{}", growing_inc(1)); // 2
    println!("{}", growing_inc(1)); // 3
    println!("{}", growing_inc(1)); // 4
}

Как видите, с каждым вызовом замыкания число, на которое замыкание увеличивает переданный в него аргумент, возрастает на единицу.

Important

Важно напомнить, что тип замыкания зависит не от того, как значение захвачено из внешнего скоупа, а от того, как это захваченное значение используется.

Замыкание из примера выше имеет тип, основанный на FnMut, не потому, что оно захватывает значение по мутабельной ссылке, а потому, что оно по мутабельной ссылке изменяет значение, чьё время жизни больше, чем один вызов замыкания.

Рассмотрим этот же пример, но только теперь наше замыкание захватит переменную step не по мутабельной ссылке, а по значению.

fn main() {
    let mut step = 1;

    // impl FnMut(i32)->i32
    let mut growing_inc = |x: i32| {
        let res = x + step;
        step += 1;
        res
    };
    println!("{}", growing_inc(1)); // 2
    println!("{}", growing_inc(1)); // 3
    println!("{}", growing_inc(1)); // 4
}

Это замыкание всё еще увеличивает шаг инкремента после каждого своего вызова, и тип замыкания всё еще реализует трэйт FnMut. Почему так? Как мы сказали: не важно, как замыкание захватило значение, важно то, как оно это значение изменяет.

Замыкание захватило переменную step по значению, и поместило в свой контекст, где это значение существует вне вызовов самого замыкания. А при вызове замыкание изменяет это значение по мутабельной ссылке.

FnOnce

После того как мы детально рассмотрели Fn и FnMut, разобраться с FnOnce должно быть несложно. Главное помнить, что тип замыкания зависит от того, что замыкание делает с захваченным значением:

  • Fn замыкания только читают захваченное значение
  • FnMut замыкания изменяют захваченное значение по мутабельной ссылке
  • FnOnce замыкание уничтожает захваченное значение

Рассмотрим следующий пример:

// Обёртка над println!(), которая принимает строку по значению,
// следовательно забирает её себе из вызывающего кода
fn print_and_destroy(s: String) {
    println!("{s}");
}

fn main() {
    let text = "text".to_string();

    // Тип замыкания: FnOnce()
    let closure_print_and_destroy = || print_and_destroy(text);

    closure_print_and_destroy();
}

Если попытаться вызвать closure_print_and_destroy() во второй раз, то компилятор выдаст ошибку:

closure cannot be invoked more than once because it moves the variable text out of its environment.

Этот тип замыканий часто используется, когда замыкание захватывает какой-то ресурс, операция над которым имеет побочный эффект: закрытие файла, очистка памяти, и другие операции, которые можно сделать только один раз.

Конкретный тип замыкания

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

То есть мы не можем написать:

fn make_inc_with_step(step: i32) -> impl Fn(i32) -> i32 {
    move |x| { x + step }
}

fn main() {
    let inc_with_5: Какой-то тип = make_inc_with_step(5);
}

Единственное, что мы можем знать о типе замыкания — то, какой трэйт он реализует и какая у замыкания сигнатура (типы аргументов и возвращаемого значения).

Note

В ночной сборке Rust при помощи флага type_alias_impl_trait можно включить псевдонимы для impl Трэйтов. Тогда можно будет писать так:

type MyFn = impl Fn(i32) -> i32;
let inc_with_5: MyFn = make_inc_with_step(5);

На момент выхода Rust 1.92 эта фича всё еще не была доступна в стабильной ветке.

Также очень важно знать, что в программе на Rust нет двух замыканий с одинаковыми типами. Даже если у двух замыканий совпадает сигнатура и трэйт, который они реализуют, то конкретные типы, которые компилятор выведет для замыканий, будут разными. Следовательно, мы не можем написать такой код:

#![allow(unused)]
fn main() {
fn make_inc(is_decrement: bool) -> impl Fn(i32) -> i32 {
    if is_decrement {
        return move |x| { x - 1 };
    } else {
        return move |x| { x + 1 };
    }
}
}

Как вы могли понять, компиляция функции завершается с ошибкой, так как impl Трэйт должно быть заменено на конкретный тип во время компиляции, а у этих двух замыканий, конкретные типы разные.

В качестве решения, мы можем использовать тот трюк, с которым познакомились в главе о Трэйтах — воспользоваться динамической диспетчеризацией. То есть вместо impl Fn возвращать dyn Fn.

fn make_inc(is_decrement: bool) -> Box<dyn Fn(i32) -> i32> {
    if is_decrement {
        Box::new(move |x| { x - 1 })
    } else {
        Box::new(move |x| { x + 1 })
    }
}
fn main() {
    let dec: Box<dyn Fn(i32) -> i32> = make_inc(true);
    let a = 2;
    let b = dec(a);
    println!("{b}"); // 1
}

Этот код компилируется и выполняется без проблем.