Замыкания: анонимные функции, которые имеют доступ к своему окружению

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

Создание обобщенного поведения используя замыкания

Рассмотрим пример, демонстрирующий сохранение замыкания для дальнейшего использования. Мы также поговорим про синтаксис замыканий, типизированный интерфейс и типажи.

Представим, что мы работаем на в стартапе, где создаём приложение для генерации планов тренировок. Серверная часть приложения создаётся на Rust, и алгоритм, который генерирует план тренировки, учитывает многие различные факторы, такие как возраст пользователей приложения, индекс массы тела, предпочтения, последние тренировки и индекс интенсивности, которые они указывают. При проектировании приложения конкретные алгоритмы реализаций не важны. Важно, чтобы различные расчёты не занимали много времени. Мы хотим использовать этот алгоритм только когда нам нужно, и делать это только один раз, чтобы не заставлять пользователя ждать больше, чем требуется. Мы будем симулировать работу алгоритма расчета параметров с помощью функции simulated_expensive_calculation (13-1), которая печатает calculating slowly..., ждёт две секунды и выводит результат расчёта:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: i32) -> i32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}
#}

Код программы 13-1: Функция, используемая для гипотетического расчёта, которой требуется около двух секунд на работу

Далее рассмотрим содержание функции main, которое содержит части нашего приложения, важные для этого примера. В примере моделируется вызов кода, который генерирует план занятий. Т.к. взаимодействие с клиентской частью программы не связано с использованием замыканий, мы также смоделируем это взаимодействие. Программно будут вводиться данные и печататься результаты.

Описание входных данных:

  • Индекс интенсивности (intensity) - определяет когда запрашивается тренировка. Этот индекс говорит о предпочтениях (низкая или высокая интенсивность)
  • Случайный номер, который будет сгенерирован для выбора плана тренировки

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

Код 13-2 показывает содержание функции main. Мы программно ввели вводимые пользователемпоказатели для простоты демонстрации работы; в реальном приложении мы бы получали значение интенсивности от фронтенда, и использовали бы пакет rand, для генерации случайного числа, как мы делали в примере игры «Угадай число» из Главы 2. Функция main вызывает функцию generate_workout с симулированными входныными значениями:

Filename: src/main.rs

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}
# fn generate_workout(intensity: i32, random_number: i32) {}

Код программы 13-2: Функция main содержащая симуляцию пользовательского ввода данных и вызов функции generate_workout

Это и есть контекст в котором мы будем работать. Функция generate_workout в примере кода 13-3 содержит логику работу программы, которую мы будем изучать в этом примере.

contains the business logic of the app that we’re most concerned with in this example. Остальные изменения в коде будут сделаны в этой функции:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
# fn simulated_expensive_calculation(num: i32) -> i32 {
#     println!("calculating slowly...");
#     thread::sleep(Duration::from_secs(2));
#     num
# }
#
fn generate_workout(intensity: i32, random_number: i32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );
        println!(
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            )
        }
    }
}
#}

Код программы 13-3: Печать плана тренировки зависит от введенных данных и вызова функции simulated_expensive_calculation

Код 13-3 несколько раз вызывает медленную функцию расчета. Первый блок if дважды вызывает simulated_expensive_calculation, if внутри внешнего else вообще не вызывает его, а код внутри else внутри внешнего else вызывает его один раз.

Желаемое поведение функции generate_workout следующее: проверка хочет ли пользователь низкой интенсивности тренировки (индекс меньше 25) или высокой (25 и более). Невысокая интенсивность будет рекомендовать количество повторений и подходов на основании сложного алгоритма, который мы моделируем функцией simulated_expensive_calculation.

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

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

Общая логика представлена. Теперь можно заняться рефакторингом кода. Для начала устраним дублирование кода. Пример первого приближения 13-4:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
# fn simulated_expensive_calculation(num: i32) -> i32 {
#     println!("calculating slowly...");
#     thread::sleep(Duration::from_secs(2));
#     num
# }
#
fn generate_workout(intensity: i32, random_number: i32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} situps!",
            expensive_result
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            )
        }
    }
}
#}

Код программы 13-4: Перенос вызова функции simulated_expensive_calculation в одно место перед блоком if и сохранение результата в переменную expensive_result

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

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

Замыкания сохраняют код, который может быть запущен позднее

Вместо того, чтобы всегда запускать функцию simulated_expensive_calculation перед блоком if, мы может определить замыкание и сохранить его в переменную. Пример 13-5:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
let expensive_closure = |num| {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};
# expensive_closure(5);
#}

Код программы 13-5: Инициализация замыкания

Определение замыкания - это часть после =, которую мы присваиваем переменной expensive_closure. Замыкание мы начинаем с пары палочек (vertical pipes (|)). Внутри этой конструкции мы определяем параметры замыкания. Такой синтаксис был выбран под влиянием языков Ruby и Smalltalk. Замыкание имеет параметр num. Несколько параметров разделяются запятыми |param1, param2|.

Далее идёт тело функции-замыкания. Фигурные скобки могут не использоваться, если код функции состоит только из одной строчки кода. После закрытия фигурных скобок необходим символ ;. Обратите внимание, что после num нет ;. Это означает, что переменная будет возращена функцией.

Также обратите внимание, что let-переменная expensive_closure содержит определение функции-замыкания, а не результат её работы. Мы используем замыкание, потому что хотим определить код для вызова в одной точке, сохранить этот код и фактически вызвать его на более позднем этапе; код, который мы хотим вызвать, теперь хранится в expensive_closure.

Теперь, после определения замыкания мы можем изменить код в блоках if, вызывая код замыкания по необходимости. Вызов замыкания очень похож на вызов функции. Мы определяем имя переменной, которая содержит определение замыкания и в скобках указываем аргументы, которые мы хотим использовать для вызова. Пример 13-6:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
fn generate_workout(intensity: i32, random_number: i32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            )
        }
    }
}
#}

Пример 13-6: Вызов замыкания expensive_closure

Мы решили задачу многократного использования (вызова) одного кода (т.е. кода объявленного в одном месте). Но мы всё-таки не решили вопрос минимизации количества вызываемого код (кэширования результата). Код по-прежнему может вызываться дважды. Этот вопрос может решить локальная переменная, объявленная в блоке if. Есть ещё более лучшее решение, к которому мы вернемся чуть позже. А сейчас обсудим почему у замыканий не может быть аннотаций типов и ещё кое-что о связях типажей и замыканий.

Интерфейс типа замыкания и аннотация (Closure Type Inference and Annotation)

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

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

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

Также как и при определении переменных, мы можем добавить описание типа данных переменных замыкания и типа возвращаемого значения (для большей информативности). Пример 13-7:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
let expensive_closure = |num: i32| -> i32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};
#}

Код 13-7: добавления описания типов данных замыкания

Синтаксис замыканий и функций больше похож на определение типов. Ниже представлено синтаксическое сравнение определения функции, которая добавляет единицу к переданому ей параметру, и замыкания, которое имеет такое же поведение. Мы добавили несколько пробелов, чтобы выровнять соответствующие части . Это иллюстрирует то, что синтаксис замыкания похож на синтаксис функции, за исключением использования пайпов ("|") вместо скобок и синтаксиса, который является необязательным:

fn  add_one_v1   (x: i32) -> i32 { x + 1 }
let add_one_v2 = |x: i32| -> i32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

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

Определения замыканий будут иметь один конкретный тип данных для каждого из параметров и выходных данных. Например (код 13-8) показывает определение замыкания и его использование. Это замыкание не очень полезно, за исключением целей этого примера. Обратите внимание, что мы не добавили никаких аннотаций типов в определение. Если мы затем попытаемся дважды вызвать замыкание, используя String в качестве аргумента, первый раз и i32 во второй раз, мы получим ошибку:

Filename: src/main.rs

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

Код 13-8: Попытка использовать замыкание с различными типами данных

Компилятор вернёт нам вот такую ошибку:

error[E0308]: mismatched types
 --> src/main.rs
  |
  | let n = example_closure(5);
  |                         ^ expected struct `std::string::String`, found
  integral variable
  |
  = note: expected type `std::string::String`
             found type `{integer}`

После того, как вы в первый раз вызвали example_closure и использовали переменные типа String, компилятор неявным образом подставит этот тип в замыкание. Этот тип данных будет неизменным у замыкания на протяжении всего времени жизни.

Использование замыканий совместно с обобщёнными типами (дженериками) и типажом Fn

Возвратимся к нашему приложению для генерации тренировочных программ. В коде 13-6 мы ещё используем неоднократно замыкание. Больше чем это нужно. Решение с кэшированием данных вычислений в переменной увеличит и усложнит наш код.

Есть ещё одно решение. Мы можем создать структуру, которая будет хранить замыкание и результат её работы. Структура выполнить код замыкания если только в этом будет необходимость. Данная структура будет кэшировать результат работы замыкания, благодаря чему в коде программы не будет необходимости в усложнении кода. Такое шаблонное решение называется запоминанием (memoization) или ленивой инициализацией (lazy evaluation).

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

Типаж Fn входит в состав стандартной библиотеки. Все замыкания реализуют один из типажей: Fn, FnMut или FnOnce. Мы поговорим о различиях между ним в следующей секции. В данном примере мы можем использовать типаж Fn.

Мы добавим типы в типаж Fn для описания типов параметров и возвращаемого значения, которое замыкания должны иметь для того, чтобы соответствовать данному типажу. В данном случае, наше замыкание имеет тип параметр i32 и возвращает i32. Сигнатура типажа имеет вид: Fn(i32) -> i32.

Код 13-9 показывает определение структуры Cacher содержащей замыкание и необязательное значение результата:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
struct Cacher<T>
    where T: Fn(i32) -> i32
{
    calculation: T,
    value: Option<i32>,
}
#}

Код 13-9: определение структуры Cacher содержащей замыкание calculation и результат в value

Структура Cacher имеет поле calculation типа T. Тип данных замыкания T описывается сигнатурой типажа Fn. Любые замыкания, которые может содержать поле calculation в экземпляре Cacher должно иметь один параметр типа i32 и возвращать i32 (определено после ->).

Поле value имеет тип Option<i32>. Перед выполнением замыкания value будет None. Если код использует структуру Cacher хочет получить результат замыкания, мы выполним замыкание и сохраним результат в значении перечисления Some. Если же код программы запросит значение замыкания ещё раз, то будет возвращено значение из Some.

Описанная логика реализована в примере кода 13-10:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# struct Cacher<T>
#     where T: Fn(i32) -> i32
# {
#     calculation: T,
#     value: Option<i32>,
# }
#
impl<T> Cacher<T>
    where T: Fn(i32) -> i32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: i32) -> i32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}
#}

Код 13-10: Реализация структуры Cacher, метода new и метода value, который управляет логикой кэширования

Поля структуры Cacher закрытые, т.к. мы хотим, чтобы экземпляр структуры управлял содержание полей и не было возможности извне каким-либо образом на это влиять. Функция Cacher::new получает обобщенный параметр T. Данная функция возвращает экземпляр структуры Cacher содержащая замыкание в поле calculation и None в поле value.

Когда вызывающий код хочет получить результат работы замыкания, вместо того чтобы вызывать замыкание непосредственно, он вызывает метод value. Этот метод проверяет есть ли уже результат работы замыкания в поле self.value внутри значения перечисления Option::Some. Если там есть значение, это значение возвращается вызывающему коду. При этом замыкание больше не используется для получения результата.

Если же поле self.value имеет значение None, то вызывается замыкание из поля self.calculation и результат работы записывается в поле self.value для будущего использования и, далее, полученное значение также возвращается вызывающему коду.

Пример кода 13-11 демонстрирует использование структуры Cacher в функции generate_workout из примера 13-6:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# use std::thread;
# use std::time::Duration;
#
# struct Cacher<T>
#     where T: Fn(i32) -> i32
# {
#     calculation: T,
#     value: Option<i32>,
# }
#
# impl<T> Cacher<T>
#     where T: Fn(i32) -> i32
# {
#     fn new(calculation: T) -> Cacher<T> {
#         Cacher {
#             calculation,
#             value: None,
#         }
#     }
#
#     fn value(&mut self, arg: i32) -> i32 {
#         match self.value {
#             Some(v) => v,
#             None => {
#                 let v = (self.calculation)(arg);
#                 self.value = Some(v);
#                 v
#             },
#         }
#     }
# }
#
fn generate_workout(intensity: i32, random_number: i32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            )
        }
    }
}
#}

Код 13-11: Использование экземпляра структуры Cacher в функции generate_workout для реализации кэширования

Вместо сохранения замыкания напрямую в переменную, мы создаём новый экземпляр структуры Cacher для хранения замыкания. Далее, в каждом месте, где необходим результат работы замыкания мы вызываем метод value экземпляра Cacher. Мы может вызывать этот метод сколько угодно или вообще не вызывать, и сложный расчет будет выполняться максимум один раз. При любом количестве вызовов функции value (одинраз или более) замыкание будет использовано только один раз. Пожалуйста, проверьте работу кода с использованием функции main.

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

Первое ограничение - предполагается, что параметр arg всегда будет одинаковым. Изменение этого условия приводит к ошибке:

#[test]
fn call_with_different_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v2, 2);
}

Этот тест создаёт новый экземпляр Cacher с замыканием и возвращает значение. Мы вызываем метод value с параметром arg со значением 1, а потом с 2. Предполагаем, что когда мы введём значение 2, то и должны получить это значение.

Тест не будет пройден:

thread 'call_with_different_arg_values' panicked at 'assertion failed:
`(left == right)` (left: `1`, right: `2`)', src/main.rs

Проблема в том, что при первом вызове c.value с аргументом 1 экземпляр Cacher сохранит значение Some(1) в self.value. После этого, неважно какие будут входные параметры. Функция всегда будет возвращать 1.

Решением будет использования хэш-таблицы вместо одного значения. Ключи будут значениями входных данных arg, а значениями будут результаты работы замыкания. Вместо того, чтобы напрямую проверять, имеет ли self.value значение Some или None, функция value будет искать arg в хэш-таблице и возвращать значение, если оно присутствует. При необходимости будет вызван код замыкания и будут произведены соответствующие вычисления.

fn value(&mut self, arg: i32) -> i32 {
        match self.value.get(&arg) {
            Some(&v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value.insert(arg,v);
                v
            },
        }
}

Ещё одним ограничением является тип данных. На данным момент ими могут быть только целочисленные значения типа i32. Мы бы хотели иметь возможность использовать различные типы данных (срезы строки, usize и другие). Попытаемся решить этот вопросы с использованием обобщенных параметров.

Замыкания могут получать доступ переменным области видимости

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

Код 13-12 демонстрирует пример переменной замыкания equal_to_x, содержание которой использует переменные в области видимости (переменная x):

Filename: src/main.rs

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
    println!("{}",equal_to_x(y));
}

Код 13-12: пример замыкания, которое использует внешнюю переменную

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

Такой функциональной возможности функции не имеют:

Filename: src/main.rs

fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool { z == x }

    let y = 4;

    assert!(equal_to_x(y));
}

Описание ошибки:

error[E0434]: can't capture dynamic environment in a fn item; use the || { ... }
closure form instead
 -->
  |
4 |     fn equal_to_x(z: i32) -> bool { z == x }
  |                                          ^

Компилятор даже напоминает нам, что это работает только с замыканиями!

Рассмотрим внутреннюю организацию работы с внешними переменными у замыканий. Для хранения данных о внешних переменных у замыканий предусмотрена хранилище (кэш). Использование данной возможности накладывает дополнительную нагрузку на ресурсы системы (память). Для большей безопасности и устойчивой работы системы было принято решение отключить у функций такою возможность.

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

  • FnOnce получает значения из области видимости (environment). Для получения доступа к переменным замыкание должно получить во владения используемые переменные. Замыкание не может получить во владение одну и туже переменную несколько раз.
  • Fn заимствует значения из среды (не изменяя при этом их значений).
  • FnMut может изменять значения переменных.

Когда мы создаём замыкание, компилятор делает выводы о целях использования переменных среды на основании используемых значений. В примере 13-12 equal_to_x получает доступ к x (readonly), т.е. замыкания реализует Fn.

Для получения владения переменными используется ключевое слово move перед списком параметров. Это удобно, когда замыкание перемещается в другой поток. Мы рассмотрим примеры использования move в Главе 16, когда будем рассматривать возможности Rust для разработки многопоточных приложений. В примере 13-12 ключевое слово move добавлено в определении замыкания и используется вектор вместо целочисленного значения. Примитивные типы (как мы знаем) могут быть скопированы (а нам надо перемещать):

Filename: src/main.rs

fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    println!("can't use x here: {:?}", x);

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

Описание ошибки:

error[E0382]: use of moved value: `x`
 --> src/main.rs:6:40
  |
4 |     let equal_to_x = move |z| z == x;
  |                      -------- value moved (into closure) here
5 |
6 |     println!("can't use x here: {:?}", x);
  |                                        ^ value used here after move
  |
  = note: move occurs because `x` has type `std::vec::Vec<i32>`, which does not
    implement the `Copy` trait

Здесь переменная x перемещена в замыкание её определении. Поэтому в функции main переменная x большое не может быть использована. Для устранения ошибки компиляции, устраните эту ошибку (например, удалите строку 6).

В большинстве случаев типаж Fn будет использован. Компилятор сам вам сообщит, когда лучшем решение было бы использовать FnMut или FnOnce (на основании использования внешних переменных замыканием).

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