Rust: Мутировать локальную среду из двух замыканий, переданных в одну и ту же функцию - PullRequest
0 голосов
/ 21 марта 2020

Смежный вопрос: Не могу заимствовать изменчиво в двух разных замыканиях в одной и той же области действия

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

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

У меня есть вложенная структура и функция, которая находит в ней элемент. Вызывающий эту функцию должен видоизменить как элемент, так и его родителя. Это создает проблему, так как я не могу вернуть & mut родителю, пока & mut ребенку позаимствован у него. Я также хочу избежать повторного обхода структуры, чтобы найти родителя после мутации ребенка. То, что я хочу, это способ «вернуть» ссылку на потомка, а затем мутировать родителя.

На данный момент мое решение заключается в том, что функция, которая находит элемент - давайте назовем его with_something - принимает два обратных вызова FnOnce: один для изменения дочернего элемента, другой для изменения родительского. Он вызывает их в таком порядке, как только находит предмет на более низком уровне. Пока все хорошо.

Проблема в том, что в этих двух обратных вызовах мне также нужно изменить локальную переменную в вызывающей функции (в частности, HashSet, чтобы отслеживать, какие элементы были изменены). Теперь компилятор мне больше не доверяет. Он считает использование двух обратных вызовов HashSet одновременными изменяемыми ссылками. Я полагаю, это потому, что они могут быть вызваны в отдельных потоках; Я не могу думать о том, как иначе это было бы проблемой (но я хотел бы быть просветленным в этом вопросе).

Вот попытка уменьшить мой код:

use std::collections::HashSet;

fn main() {
    let mut touched_items: HashSet<i32> = HashSet::new();
    with_something(
        |inner_inner| {
            touched_items.insert(inner_inner.value);
            inner_inner.value = 10;
        },
        |inner| {
            touched_items.insert(inner.value);
            inner.value = 20;
        },
    );
    println!("touched_items: {:?}", touched_items);
}

fn with_something<F, G>(f: F, g: G)
where
    F: FnOnce(&mut InnerInnerState),
    G: FnOnce(&mut InnerState),
{
    let mut state = SomethingWithState {
        value: 5,
        inner: InnerState {
            value: 17,
            inner: InnerInnerState { value: 40 },
        },
    };
    f(&mut state.inner.inner);
    g(&mut state.inner);
    state.value = 50;
}

struct SomethingWithState {
    value: i32,
    inner: InnerState,
}

struct InnerState {
    value: i32,
    inner: InnerInnerState,
}

struct InnerInnerState {
    value: i32,
}

И ошибка:

error[E0499]: cannot borrow `touched_items` as mutable more than once at a time
  --> src/main.rs:10:9
   |
5  |     with_something(
   |     -------------- first borrow later used by call
6  |         |inner_inner| {
   |         ------------- first mutable borrow occurs here
7  |             touched_items.insert(inner_inner.value);
   |             ------------- first borrow occurs due to use of `touched_items` in closure
...
10 |         |inner| {
   |         ^^^^^^^ second mutable borrow occurs here
11 |             touched_items.insert(inner.value);
   |             ------------- second borrow occurs due to use of `touched_items` in closure

Есть ли способ, которым я могу объявить with_something, чтобы компилятор знал, что два замыкания выполняются последовательно и до возврата из функции, и, следовательно, безопасно принимать изменяемый ref к тому же в каждом? (Или я что-то неправильно понимаю, и на самом деле был бы способ сделать что-то небезопасное?)

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

Мой обходной путь - который я нашел в процессе написания этого вопроса, так что спасибо за это - проходит состояние окружающей среды, которое мне нужно преобразовать в with_something и обратно в замыкания как & mut. Это то, что я сейчас делаю, так как он поддерживает общий поток кода в вызывающей функции. Я сделал его обобщенным c относительно типа состояния, чтобы избежать слишком тесного связывания with_something со своими пользователями, но он все еще ощущается как излишне грязный API.

fn main() {
    // ...
    with_something(
        &mut touched_items,
        |inner_inner, touched_items| {
            touched_items.insert(inner_inner.value);
            inner_inner.value = 10;
        },
        |inner, touched_items| {
            touched_items.insert(inner.value);
            inner.value = 20;
        },
    );
    // ...
}

fn with_something<F, G, S>(pass_through_state: &mut S, f: F, g: G)
where
    F: FnOnce(&mut InnerInnerState, &mut S),
    G: FnOnce(&mut InnerState, &mut S),
{
    // ...
    f(&mut state.inner.inner, pass_through_state);
    g(&mut state.inner, pass_through_state);
    // ...
}

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

Я также рассмотрел создание свойства, определяющего два обратных вызова с помощью & mut self, и реализацию его со структурой, которая содержит HashSet. Но это по сути то же самое, что и прохождение через & mu. Я полагаю, что на первый взгляд функция подписи функции будет выглядеть чище, на самом деле это не будет лучше API, а на сайте вызовов будет более сложный и нестандартный код, что, похоже, не стоит.

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

...