Использование компилятора Rust, чтобы не забыть вызвать метод

У меня есть такой код:

foo.move_right_by(10);
//do some stuff
foo.move_left_by(10);

Очень важно выполнять обе эти операции в конце концов, но я часто забываю сделать вторую после первой. Это вызывает много ошибок, и мне интересно, есть ли идиоматический способ Rust избежать этой проблемы. Есть ли способ заставить компилятор rust сообщать мне, когда я забуду?

Моя идея заключалась в том, чтобы как-то иметь что-то вроде этого:

// must_use will prevent us from forgetting this if it is returned by a function
#[must_use]
pub struct MustGoLeft {
    steps: usize;
}

impl MustGoLeft {
    fn move(&self, foo: &mut Foo) {
        foo.move_left_by(self.steps);
    }
}

// If we don't use left, we'll get a warning about an unused variable
let left = foo.move_left_by(10);

// Downside: move() can be called multiple times which is still a bug
// Downside: left is still available after this call, it would be nice if it could be dropped when move is called
left.move();

Есть ли лучший способ сделать это?

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

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

foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.close_box();

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

У нас может быть даже что-то вроде этого:

foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.move_right_by(10);
foo.close_box();
foo.move_left_by(10);
// do more stuff...

person Sunjay Varma    schedule 04.02.2017    source источник
comment
Просто для уточнения: оба метода должны вызываться в какой-то момент внутри закрывающей функции? Актуален ли заказ? Или вы просто хотите убедиться, что если вызывается move_right(), то и move_left() тоже вызывается? Также: можете ли вы описать закрывающую функцию? Возвращает что-нибудь?   -  person Lukas Kalbertodt    schedule 04.02.2017
comment
OP, если вам нужно вызвать move_left() после вызова move_right(), вы можете связать эти функции, чтобы вернуть пользовательский тип (своего рода обработчик), который автоматически вызывает move_left() на Drop — хотя это может быть невозможно, если ваш второй fn должен возвращать значения и так далее... Это может быть идеей, но она может довольно быстро стать уродливой.   -  person musicmatze    schedule 04.02.2017
comment
Если вы можете выразить последовательность вызовов, которые должны быть выполнены, как конечный автомат, то состояния можно закодировать как типы, а переходы — как вызовы методов на self (которые потребляют текущее состояние и создают новое). Возможно ли это или недостаточно гибко?   -  person Matthieu M.    schedule 04.02.2017
comment
@LukasKalbertodt Это второе: если вы двигаетесь вправо, вы должны потом двигаться влево. Это не может быть Drop, как то, что предложил musicmatze, потому что это полагалось бы на то, что компилятор отбросит значение в нужное время. Нам нужно больше контроля, чем это. Охватывающей функцией будут другие операции, которые также перемещаются вправо, а затем влево на ту же величину. Важно всегда в конце концов возвращаться к ссылке, снова перемещаясь влево.   -  person Sunjay Varma    schedule 04.02.2017


Ответы (3)


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

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

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

use std::marker::PhantomData;

pub struct GoneLeft;
pub struct GoneRight;
pub type Completed = (GoneLeft, GoneRight);

pub struct Thing<S = ((), ())> {
    pub position: i32,
    phantom: PhantomData<S>,
}


// private to control how Thing can be constructed
fn new_thing<S>(position: i32) -> Thing<S> {
    Thing {
        position: position,
        phantom: PhantomData,
    }
}

impl Thing {
    pub fn new() -> Thing {
        new_thing(0)
    }
}

impl<L, R> Thing<(L, R)> {
    pub fn move_left_by(self, by: i32) -> Thing<(GoneLeft, R)> {
        new_thing(self.position - by)
    }

    pub fn move_right_by(self, by: i32) -> Thing<(L, GoneRight)> {
        new_thing(self.position + by)
    }
}

Вы можете использовать его следующим образом:

// This function can only be called if both move_right_by and move_left_by
// have been called on Thing already
fn do_something(thing: &Thing<Completed>) {
    println!("It's gone both ways: {:?}", thing.position);
}

fn main() {
    let thing = Thing::new()
          .move_right_by(4)
          .move_left_by(1);
    do_something(&thing);
}

И если вы пропустите один из необходимых методов,

fn main(){
    let thing = Thing::new()
          .move_right_by(3);
    do_something(&thing);
}

то вы получите ошибку компиляции:

error[E0308]: mismatched types
  --> <anon>:49:18
   |
49 |     do_something(&thing);
   |                  ^^^^^^ expected struct `GoneLeft`, found ()
   |
   = note: expected type `&Thing<GoneLeft, GoneRight>`
   = note:    found type `&Thing<(), GoneRight>`
person Peter Hall    schedule 04.02.2017
comment
Вот интересный подход! Отличный способ привлечь компилятор, чтобы убедиться, что это происходит. - person Sunjay Varma; 04.02.2017

Я не думаю, что #[must_use] действительно то, что вам нужно в этом случае. Вот два разных подхода к решению вашей проблемы. Первый — просто заключить то, что вам нужно сделать, в замыкание и абстрагироваться от прямых вызовов:

#[derive(Debug)]
pub struct Foo {
    x: isize,
    y: isize,
}

impl Foo {
    pub fn new(x: isize, y: isize) -> Foo {
        Foo { x: x, y: y }
    }

    fn move_left_by(&mut self, steps: isize) {
        self.x -= steps;
    }

    fn move_right_by(&mut self, steps: isize) {
        self.x += steps;
    }

    pub fn do_while_right<F>(&mut self, steps: isize, f: F)
        where F: FnOnce(&mut Self)
    {
        self.move_right_by(steps);
        f(self);
        self.move_left_by(steps);
    }
}

fn main() {
    let mut x = Foo::new(0, 0);
    println!("{:?}", x);
    x.do_while_right(10, |foo| {
        println!("{:?}", foo);
    });
    println!("{:?}", x);
}

Второй подход заключается в создании типа оболочки, который вызывает функцию при удалении (аналогично тому, как Mutex::lock создает MutexGuard, который разблокирует Mutex при удалении):

#[derive(Debug)]
pub struct Foo {
    x: isize,
    y: isize,
}

impl Foo {
    fn new(x: isize, y: isize) -> Foo {
        Foo { x: x, y: y }
    }

    fn move_left_by(&mut self, steps: isize) {
        self.x -= steps;
    }

    fn move_right_by(&mut self, steps: isize) {
        self.x += steps;
    }

    pub fn returning_move_right(&mut self, x: isize) -> MovedFoo {
        self.move_right_by(x);
        MovedFoo {
            inner: self,
            move_x: x,
            move_y: 0,
        }
    }
}

#[derive(Debug)]
pub struct MovedFoo<'a> {
    inner: &'a mut Foo,
    move_x: isize,
    move_y: isize,
}

impl<'a> Drop for MovedFoo<'a> {
    fn drop(&mut self) {
        self.inner.move_left_by(self.move_x);
    }
}

fn main() {
    let mut x = Foo::new(0, 0);
    println!("{:?}", x);
    {
        let wrapped = x.returning_move_right(5);
        println!("{:?}", wrapped);
    }
    println!("{:?}", x);
}
person Erik Vesteraas    schedule 04.02.2017
comment
Спасибо за Ваш ответ! К сожалению, метод закрытия не работает, потому что он должен быть немного более гибким. Я добавил еще один пример в вопрос, чтобы уточнить. Второй подход интересен, но что, если я хочу явно выполнить обратную операцию явно? Трудно полагаться на то, что компилятор уложится во времени. - person Sunjay Varma; 04.02.2017
comment
@SunjayVarma в Rust, из-за того, как работает владение, удаление должно быть полностью детерминированным; также вы можете обернуть исходный Foo в некоторый тип, который имеет метод drop(), и вы можете убедиться, что право собственности на этот объект-оболочку выходит из области видимости в нужное время. Мне кажется, что решение на основе капель, вероятно, правильный путь. - person djc; 06.02.2017
comment
Соглашусь, что бросать - правильно. Если вам действительно нужно, чтобы удаление произошло до конца области, вы также можете просто вызвать пустую функцию, которая получает право собственности на значение, например здесь: gist.github.com/silmeth/f1d66c819862b418fd8862e3dc36875a — но если вы забудете сделать это вручную, отбрасывание произойдет в конце области видимости, поэтому вы не пропустить вызов метода move_right(). - person silmeth; 06.02.2017
comment
Когда я сказал, что трудно полагаться на то, что компилятор отбрасывает значение во времени, я имел в виду, что иногда мне нужно отбрасывать значение явно. Хотя отбрасывание является полностью детерминированным, оно обычно происходит в конце области видимости, поэтому, хотя это и достигает цели вызова метода в конечном итоге, оно не позволяет мне явно увидеть, когда метод был вызван. Спасибо за ответы, дискуссия очень интересная! - person Sunjay Varma; 06.02.2017

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

Новый тип может запрещать/требовать выполнения различных вызовов перед переходом в состояние завершения. Например (не проверено):

struct CanGoRight { .. }
impl CanGoRight {
    fn move_right_by(self, steps: usize) -> MustGoLeft {
        // Note: self is consumed and only `MustGoLeft` methods are allowed
        MustGoLeft{steps: steps}
    }
}
struct MustGoLeft {
    steps: usize;
}
impl MustGoLeft {
    fn move_left_by(self, steps: usize) -> Result<CanGoRight, MustGoLeft> {
        // Totally making this up as I go here...
        // If you haven't moved left at least the same amount of steps,
        // you must move a bit further to the left; otherwise you must
        // switch back to `CanGoRight` again
        if steps < self.steps {
            Err(MustGoLeft{ steps: self.steps - steps })
        } else {
            Ok(CanGoRight{ steps: steps - self.steps })
        }
    }
    fn open_box(self) -> MustGoLeftCanCloseBox {..}
}

let foo = foo.move_right_by(10); // can't move right anymore

В этот момент foo больше не может двигаться вправо, поскольку это не разрешено MustGoLeft, но он может двигаться влево или открывать коробку. Если он перемещается влево достаточно далеко, он снова возвращается в состояние CanGoRight. Но если он открывает коробку, то применяются совершенно новые правила. В любом случае вам придется иметь дело с обеими возможностями.

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

В конце концов, это звучит так, как будто вы делаете своего рода конечную машину. Возможно, https://hoverbear.org/2016/10/12/rust-state-machine-pattern/ будет полезен.

person Tommi Komulainen    schedule 08.02.2017
comment
Это похоже на мой ответ, но на самом деле немного проще. Я думаю, что есть несколько подобных подходов, и вы, вероятно, могли бы даже создать макрос, который мог бы генерировать состояния и переходы на уровне типов. - person Peter Hall; 03.03.2017