% Замыкания

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

Синтаксис

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

let plus_one = |x: i32| x + 1;

assert_eq!(2, plus_one(1));

Мы создаем связывание, plus_one, и присваиваем ему замыкание. Аргументы замыкания располагаются между двумя символами |, а телом замыкания является выражение, в данном случае: x + 1. Помните, что { } также является выражением, поэтому тело замыкания может содержать много строк:

let plus_two = |x| {
    let mut result: i32 = x;

    result += 1;
    result += 1;

    result
};

assert_eq!(4, plus_two(2));

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

let plus_one = |x: i32| -> i32 { x + 1 };

assert_eq!(2, plus_one(1));

Но мы не должны. Почему так? В основном, это было сделано из эргономических соображений (соображений удобства). В то время как для именованных функций явное указание типа является полезным для таких аспектов как документация и вывод типа, типы замыканий редко документируют, поскольку они анонимны. К тому же, они не вызывают «ошибок на расстоянии» (error-at-a-distance), которые могут вызывать именованные функции. Такие ошибки могут возникать, когда локальное изменение (например, в теле одной из функций) вызывает изменение вывода типов. Компилятор пытается подобрать типы в окружающей программе под уже другие типы в изменённой функции, и часто оказывается, что имена имеют другие типы, нежели мы ожидали. В результате происходит ошибка «на расстоянии» — возможно, в другой функции, использующей изменённую.

Второе отличие — синтаксис очень похож, но все же немного отличается. Мы добавили пробелы здесь, чтобы было нагляднее:

fn  plus_one_v1   (x: i32 ) -> i32 { x + 1 }
let plus_one_v2 = |x: i32 | -> i32 { x + 1 };
let plus_one_v3 = |x: i32 |          x + 1  ;

Есть небольшие различия, но принцип аналогичен.

Замыкания и их окружение

Замыкания называются так потому, что они 'замыкают свое окружение.' Это выглядит следующим образом:

let num = 5;
let plus_num = |x: i32| x + num;

assert_eq!(10, plus_num(5));

Это замыкание, plus_num, ссылается на связанную с помощью оператора let переменную num, расположенную в своей области видимости. Если говорить более конкретно, то оно заимствует связывание. Если мы сделаем что-то, что противоречило бы связыванию, то получим ошибку. Например этот код:

let mut num = 5;
let plus_num = |x: i32| x + num;

let y = &mut num;

Который выдаст следующие ошибки:

error: cannot borrow `num` as mutable because it is also borrowed as immutable
    let y = &mut num;
                 ^~~
note: previous borrow of `num` occurs here due to use in closure; the immutable
  borrow prevents subsequent moves or mutable borrows of `num` until the borrow
  ends
    let plus_num = |x| x + num;
                   ^~~~~~~~~~~
note: previous borrow ends here
fn main() {
    let mut num = 5;
    let plus_num = |x| x + num;

    let y = &mut num;
}
^

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

let mut num = 5;
{
    let plus_num = |x: i32| x + num;

} // plus_num goes out of scope, borrow of num ends

let y = &mut num;

Однако, Rust также может забирать право владения и перемещать свое окружение, если этого требует замыкание:

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

let takes_nums = || nums;

println!("{:?}", nums);

Этот код выдаст:

note: `nums` moved into closure environment here because it has type
  `[closure(()) -> collections::vec::Vec<i32>]`, which is non-copyable
let takes_nums = || nums;
                    ^~~~~~~

Vec<T> обладает правом владения на свое содержимое, и поэтому, когда мы ссылаемся на него в нашем замыкании, мы должны забрать право владения на nums. Это тоже самое, как если бы мы передавали nums в функцию, которая забирала бы право владения на него.

Перемещающие замыкания (move closures)

Мы можем заставить наше замыкание забирать право владения на свое окружение с помощью ключевого слова move:

let num = 5;

let owns_num = move |x: i32| x + num;

Теперь, когда указано ключевое слово move, переменные следуют нормальной семантике перемещения. В данном примере 5 реализует Copy, поэтому owns_num становится владельцем копии num. Так в чем же разница?

let mut num = 5;

{
    let mut add_num = |x: i32| num += x;

    add_num(5);
}

assert_eq!(10, num);

Итак, в этом примере наше замыкание принимает изменяемую ссылку на num. Затем, когда мы вызываем замыкание add_num, то, как мы и ожидали, оно изменяет значение внутри. Нам также необходимо объявить add_num как mut, потому что оно изменяет свое окружение.

Если же мы будем использовать move замыкание, то получим следующие отличия:

let mut num = 5;

{
    let mut add_num = move |x: i32| num += x;

    add_num(5);
}

assert_eq!(5, num);

Мы всего лишь получаем 5. Вместо того, чтобы получать изменяемый заем на num, мы получаем право владения на копию.

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

Но прежде чем говорить о получении в качестве аргумента и возвращении замыкания, мы должны поговорить о том, как реализуются замыкания. Как системный язык программирования, Rust дает вам кучу контроля над тем, что делает ваш код, и замыкания не являются исключением.

Реализация замыканий

Реализация замыканий в Rust немного отличается от других языков. Фактически, она представляет из себя просто синтаксический сахар для типажей. Перед тем как читать дальше, настоятельно рекомендуем изучить главу Типажи, а также главу Типажи-объекты, в которой говорится о типажах-объектах.

Изучили? Хорошо.

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

# mod foo {
pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
# }

Вы можете заметить некоторые различия между этими типажами, но есть одно главное различие — self: Fn принимает &self, FnMut принимает &mut self, FnOnce принимает self. Это покрывает все три вида self с помощью обычного синтаксиса вызова методов. Мы разделили их на три типажа, вместо того, чтобы иметь один. Это дает нам большее количество контроля над тем, какого вида замыкания мы можем принять.

Использование || {} при создании замыканий является синтаксическим сахаром для этих трех типажей. Rust будет генерировать структуру для окружения, реализующую (impl) соответствующий типаж, а затем использовать его.

Передача замыканий в качестве аргументов

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

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

fn call_with_one<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {

    some_closure(1)
}

let answer = call_with_one(|x| x + 2);

assert_eq!(3, answer);

Мы передаем наше замыкание |x| x + 2, в функцию call_with_one. Она же делает то, о чем говорит ее название: вызывает замыкание, передавая ему 1 в качестве аргумента.

Давайте рассмотрим сигнатуру функции call_with_one более подробно:

fn call_with_one<F>(some_closure: F) -> i32
#    where F : Fn(i32) -> i32 {
#    some_closure(1) }

Мы принимаем один параметр, который имеет тип F. Мы также возвращаем i32. Эта часть не интересна. Следующим важным моментом является:

# fn call_with_one<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {
#   some_closure(1) }

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

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

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

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}

let answer = call_with_one(&|x| x + 2);

assert_eq!(3, answer);

Теперь наша функция в качетве аргумента принимает типаж-объект &Fn. Поэтому мы должны создать ссылку на замыкание, а затем передать ее в функцию call_with_one, для этого мы используем &||.

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

Что очень характерно для кода в функциональном стиле — возвращать замыкания в различных ситуациях. Если вы попытаетесь вернуть замыкание, то можете столкнуться с ошибкой. Сперва это может показаться странным, но мы с этим разберемся. Вот как вы, наверное, попытаетесь вернуть замыкание из функции:

fn factory() -> (Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

Это выдаст следующие длинные, взаимосвязанные ошибки:

error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> i32` [E0277]
fn factory() -> (Fn(i32) -> i32) {
                ^~~~~~~~~~~~~~~~
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
fn factory() -> (Fn(i32) -> i32) {
                ^~~~~~~~~~~~~~~~
error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn(i32) -> i32` [E0277]
let f = factory();
    ^
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
let f = factory();
    ^

Для того чтобы вернуть что-то из функции, Rust должен знать, какой размер имеет тип возвращаемого значения. Но так как Fn является типажом, то в качестве него могут выступать совершенно разные объекты, с разными размерами: много различных типов могут реализовать Fn. Самый простой способ передать что-то неопределенного размера — передать ссылку на это что-то, так как ссылки имеют известный размер. Таким образом, следовало бы написать так:

fn factory() -> &(Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

Но тогда мы получим другую ошибку:

error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
                ^~~~~~~~~~~~~~~~~

Верно. Так как у нас используется ссылка, то мы должны задать ее время жизни. Так наша функция factory() не принимает никаких аргументов, то элизия (сокрытие) здесь не уместна. Какое время жизни мы должны выбрать? 'static:

fn factory() -> &'static (Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

Но мы получим еще ошибку:

error: mismatched types:
 expected `&'static core::ops::Fn(i32) -> i32`,
    found `[closure <anon>:7:9: 7:20]`
(expected &-ptr,
    found closure) [E0308]
         |x| x + num
         ^~~~~~~~~~~

Эта ошибка сообщает нам, что ожидается использование &'static Fn(i32) -> i32, а используется [closure <anon>:7:9: 7:20]. Подождите, что?

Поскольку каждое замыкание (в индивидуальном порядке) генерирует свою собственную struct для окружения и реализует Fn и компанию, то эти типы являются анонимными. Они существуют исключительно для этого замыкания. Поэтому Rust показывает их как closure <anon>, а не в виде какого-то автоматически сгенерированного имени.

Но почему же наше замыкание не реализует &'static Fn? Как мы обсуждали ранее, замыкание заимствует свое окружение. И в этом случае наше окружение представляет собой выделенную в стеке память, содержащую значение связанной переменной num - 5. Из-за этого заем имеет срок жизни фрейма стека. Так что, когда мы вернем это замыкание, то вызов функции будет завершен, а фрейм стека уйдет, и наше замыкание захватит окружение, содержащее в памяти мусор!

Так что же делать? Этот код почти работает:

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;

    Box::new(|x| x + num)
}
# fn main() {
let f = factory();

let answer = f(1);
assert_eq!(6, answer);
# }

Мы используем типаж-объект, полученный в результате упаковки (Box) типажа Fn. И остаётся только одна, последняя проблема:

error: closure may outlive the current function, but it borrows `num`,
which is owned by the current function [E0373]
Box::new(|x| x + num)
         ^~~~~~~~~~~

Мы все еще по-прежнему ссылаемся на родительский фрейм стека. С этим последним исправлением мы сможем наконец выполнить нашу задачу:

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;

    Box::new(move |x| x + num)
}
# fn main() {
let f = factory();

let answer = f(1);
assert_eq!(6, answer);
# }

Благодаря изменению внутреннего замыкания на move Fn будет создаваться новый фрейм стека для нашего замыкания. А благодаря упаковке (Box) замыкания, получается известный размер возвращаемого значения, и позволяет ему избежать (быть независимым от) нашего фрейма стека.

results matching ""

    No results matching ""