После моего последнего рейда в стандартную библиотеку Rust, который оставил у меня глубокое удовлетворение, я остановился на закрытии. Это тема для следующей главы, поэтому я пролистал главу IO - там не было неизвестных идей. Я вернусь к нему после того, как закончу с закрытием. Сегодняшняя глава - это глава о замыканиях и итераторах.

Замыкания - это… лямбды?

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

В большинстве языков это называется «лямбда», не так ли? Пример Python: foo = lambda x: x+2.

Я всегда тороплюсь с выводами… Следующая фраза из книги:

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

Да, это больше похоже на закрытие.

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

fn main() {
    let x = 3;
    let foo = || x;
    let x = 4;
    println!("{}, {}", foo(), x);
}

Он печатает 3, 4 (как и ожидалось) и прост для понимания. Замыкание сохраняет значения внутри себя замороженными.

Попробуем поиграть с прицелом.

fn  main() {
    let foo = || {
        let x = 3;
        || x
    };
    println!("{}", foo()());
}

Неа!

error[E0597]: `x` does not live long enough
 --> src/main.rs:4:12
  |
4 |         || x
  |         -- ^ borrowed value does not live long enough
  |         |
  |         capture occurs here
5 |     };
  |     - borrowed value only lives until here
...
8 | }
  | - borrowed value needs to live until here

Я бы сказал, проницательный компилятор. Небольшая модификация заставляет все работать:

fn  main() {
    let x = 3;
    let foo = || {
        || x
    };
    println!("{}", foo()());
}

Чтобы усвоить идею закрытия, я говорю, что:

Замыкания в Rust - это лямбды со встроенными замыканиями.

Общие закрытия?

Я продолжаю следить за главой в книге. Замыкание похоже на лямбда-выражения Python в одном определенном смысле: у них нет аннотаций типов, как в любом языке с динамической типизацией. Но в Rust нет динамической типизации.

Вывод типа - наш друг. Давайте проверим, что замыкания не набираются динамически:

fn  main() {
    let foo = |x| x;
    println!("{}, {}", foo(1), foo("test"));
}

Результат:

3 |     println!("{}, {}", foo(1), foo("test"));
  |                                    ^^^^^^ expected integral variable, found reference

На самом деле, не с динамической типизацией. Но у нас есть дженерики ...

  • пусть foo = ‹T› | x: T | Икс;
  • lef foo = | x: ‹T› | Икс;
  • пусть foo: T = ‹T› | x: T | Икс;
  • (еще несколько сумасшедших попыток, которые я стесняюсь опубликовать)

Неа. Ничего не получилось.

У SO есть ответ на мою идею:

Привязка let не становится мономорфной, поэтому у вас не может быть let a<T> = ... и другой версии a, созданной для вас компилятором.

Тот же ответ сулил в будущем кое-что интересное:

// does not work as of Rust 1
let a = for<T> |s: &str, t: T| {...}

Но он действительно не компилируется в моей текущей версии Rust (1.24.1 + dfsg1–1).

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

Черты с параметрами

На предыдущем сеансе я боролся с чертой AsRef. Теперь книга представила мне простую T: Fn(u32) -> u32 черту, которая многое объясняет. Синтаксис трейса настолько хорош, что позволяет запрашивать тип аргумента, который имеет реализацию с определенной сигнатурой.

Дженерики и имп

Дальнейшее чтение указывало мне на новое общее использование для impl:

impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn ...
    ...
}

Насколько я понял, в нем говорится, что реализация для структуры Cacher ограничена тем же типом T, что и для Cacher. Очень откровенно.

двигаться к закрытию

Я нашел еще один поворот к закрытию. Ключевое слово move находится в Rust.

Но если я не буду использовать move для закрытия, что произойдет?

let mut y = 3;
let foo = |x| x + y;
y = 4;

Да, это ошибка «мутации неизменного заимствования»:

error[E0506]: cannot assign to `y` because it is borrowed
 --> src/main.rs:4:5
  |
3 |     let foo = |x| x + y;
  |               --- borrow of `y` occurs here
4 |     y = 4;
  |     ^^^^^ assignment to borrowed `y` occurs here

Результат очередного эксперимента оказался для меня совершенно неожиданным.

fn  main() {
    let mut y = 3;
    let foo = move |x| x + y;
    y = 4;
    println!("{}, {}", foo(2), y);
}

Он компилируется, он работает (вывод «5, 4»), но почему Rust позволяет мне перемещать значение (перемещение - это передача права собственности, верно?), А затем разрешать присваивать значение переменной «перемещено»?

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

Можем ли мы проделать тот же трюк без замыканий?

Начнем с этого примера:

fn foo<T>(x:T) -> T{
    x
}
fn  main() {
    let mut y = 3;
    let z = foo(y);
    y = 4;
    println!("{}, {}", z, y);
}

Он работает и компилируется, как ожидалось.

Если мы добавим заимствование, оно перестанет работать.

fn foo<T>(x:&T) -> &T{
    x
}
fn  main() {
    let mut y = 3;
    let z = foo(&y);
    y = 4;
    println!("{}, {}", z, y);
}

Ошибка:

error[E0506]: cannot assign to `y` because it is borrowed
 --> src/main.rs:8:5
  |
7 |     let z = foo(&y);
  |                  - borrow of `y` occurs here
8 |     y = 4;
  |     ^^^^^ assignment to borrowed `y` occurs here

Ожидается и то, и другое, и я понимаю, почему одно работает, а другое - нет.

Но вместе они нарушают мое неявное предположение о том, что передача права собственности более ограничительна, чем заимствование. Это неправда.

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

Предоставление стоимости в кредит более ограничивает собственника, чем передача права собственности. Работа с переданной стоимостью - проблема получателя, а работа с заимствованной стоимостью - обязанность владельца. Работа с не заемной стоимостью менее строгая, чем с заимствованной.

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

Я возвращаюсь к закрытию move. Теперь это разумно: мы используем move, чтобы передать право собственности на закрытие, а без move мы вместо этого занимаем. А мы? Давай я это проверю.

Я показываю вам один (неправильный) пример, с которым я пытался поиграть. Это не та вещь, которую я хочу проверять, но я сохраню ее, чтобы сохранить хронологию своих попыток.

let mut y = 3;
let z = |x| x;
let l = z(y);
y = 4;
println!("{}, {}", l, y);

Если я заменю закрытие на let z = move |x| x;, ничего не изменится. Оба примера компилируемы, оба примера дают одинаковый результат. Но между этими примерами нет никакой разницы, потому что в обоих случаях нет фактических закрытий. Значение рассчитывается в рамках y=3, поэтому я могу просто написать l=y, и ничего не изменится.

Вот исправленный пример. Это работает, только если я добавлю «move» к закрытию.

let mut y = 3;
let z = |x| y;
y = 4;
println!("{}, {}", z(1), y);

Ошибка:

error[E0506]: cannot assign to `y` because it is borrowed
 --> src/main.rs:4:5
  |
3 |     let z = |x| y;
  |             --- borrow of `y` occurs here
4 |     y = 4;
  |     ^^^^^ assignment to borrowed `y` occurs here
error: aborting due to previous error

Это подтверждает мою модель:

Закрытие без «перемещения» заимствует значения, закрытие с «перемещением» передает значения в себя.

Сегодняшний финал

Я думал, что напишу сегодня целую главу, но нет. Я узнал о (лямбда) замыканиях в Rust и, что более важно, исправил свою интерпретацию модели владения / заимствования. В следующий раз, когда я закончу эту главу (итераторы), я вернусь к главе IO, чтобы продолжить изучение сценария «чтения файла» вплоть до уровня libc / syscall.