Комментарии 28

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

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

Спасибо! В Rust есть особый паттерн "хватить раст и советовать всем на него переходить", но при этом сам переход куда менее романтичен, чем его описывают. Вот такие статьи куда полезнее 100500 рекламы "языка без GC".

Зачем такое нужно не понятно. Чем это лучше enum?
На уровень типов ничего не поднялось, потому что все равно можно вызвать не доступную операцию из трейта state. Она вернёт None, но от этого не сильно легче.
Зачем-то обернули всё боксами. Предполагается ли тут полиморфизм и дальнейшее расширение не понятно, вроде бы нет т.к. наружу торчит только pub struct Article.


Upd. Окей я понял в чём смысл этого паттерна. Получается такая транспонированная таблица переходов. Вместо порядка функция -> начальное состояние -> конечное состояние, мы пишем начальное состояние -> функция -> конечное состояние. А инверсия достигается через vtable.… но зачем?)

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

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

Видимо по той причине, что нельзя определить метод для конкретного состояния в Enum.

Так в предложенном коде методы тоже есть у всех состояний Enum (находятся в трейте Article).

ну, так в случае с enum придется делать что-то вроде


enum E {
    State1,
    State2
}
impl E {
  fn some_overlapping_fn(self) -> Result<_,_> {
    match self {
       State1 -> {..},
       State2 -> {..}
  }
   ...
}

А с типажами каждая из реализаций будет изолирована от остальных. Зато вариант ниже с шаблонной структурой вполне подходит.

А с типажами каждая из реализаций будет изолирована от остальных.

Так бОльшая часть реализаций будет дефолтная (по крайней мере, в сегодняшнем примере сделано так).

Можно так сделать.


struct State<T>(T);

struct FirstState {
    foo: i32,
}

struct SecondState {
    boo: i64,
}

impl State<FirstState> {
    fn make_first_op(self) -> State<SecondState> {
        State(SecondState {
            boo: self.0.foo as i64,
        })
    }
}

impl State<SecondState> {
    fn make_second_op(self) -> State<FirstState> {
        State(FirstState {
            foo: self.0.boo as i32,
        })
    }
}

Любопытно, а есть какие-то советы, премы, best-practice, прости господи, паттерны проектирования для Rust? Чтобы не пытаться натягивать ООП-опыт на другую систему типов, а получать от нее максимальную пользу?

AFAIK пока нету. Кроме rust-book, и что-то такого начального уровня. В расте добавили борроу чекер, убрали часть ООП, добавили какие-то фп фишки и теперь придумывают что с этим делать :)

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

А можете поделиться ссылками на какие-нибудь расто-блогги из тех, что вы читаете?

Вот кстати да, первое с чем я столкнулся в расте это, то что мой предыдущей опыт по построению архитектуры на с++ тут не работает :) И очень часто вообще не понятно, что делать в той или иной ситуации, ощущение что ходить заново учишься, с одной стороны приятно т.к. что-то реально новое и не привычное — расширяет кругозор, с другой стороны когда задачу все же решить нужно и желательно за вменяемое время, а она не решается :)…
Чем это лучше enum?

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


  1. при использовании enum добавление одного нового состояния изменяет весь код, где используется наш enum. Хотя бы потому, что match должен быть exhaustive, а одними if let не везде можно обойтись
  2. с использованием паттерна код, исполняющийся при каком-то конкретном состоянии, сосредоточен в одном месте. Без него, наоборот, в одном месте сосредоточен код, исполняющийся при обработке какого-то конкретного сообщения (/ вызове метода). Как правило, этот код занимает больше места. Возможность хранить данные внутри перечисления упрощает ситуацию, но сам match на все, например, 600 состояний никуда не денется.

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

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

при использовании enum добавление одного нового состояния изменяет весь код, где используется наш enum. Хотя бы потому, что match должен быть exhaustive

Возможно не очень понял этот момент, а что мешает использовать в составе match:
_ => ()
для избежания переписывания при добавлении нового состояния?

Ничего не мешает, но так будет сложнее найти места, где из-за добавления нового состояния нужны какие-то осмысленные изменения: если в match нет такого catch-all, он просто не скомпилируется. Это, собственно, одна из причин, по которой в Rust матчи exhaustive.

Собственно, это вопрос того, что чаще добавляется — состояния или действия.

Либо просто добавлять состояния (и указывать при добавлении все не-дефолтные действия) — это вариант, предложенный в статье.
Либо просто добавлять действия (и указывать при добавлении все состояния, поведение в которых не-дефолтные) — это вариант с enum.

Ну у вас трейты по умолчанию реализованы так что они ничего не делают. Т.е. аналогично ветке _ => () в расте.
Если добавляется новое состояние, то скорее всего добавляется и новый метод, т.е. все равно придется везде менять.
Ну и контр пример на плюсах для раста не очень убедителен.

контр пример на плюсах для раста не очень убедителен

А что изменилось бы, если бы тот код писался на Расте?


И, кажется, мы в этом треде больше говорим не про смысл использования паттерна State в Rust, а про смысл использования этого паттерна вообще, потому что я не вижу, в чем растовские перечисления лучше перечислений в C++/Java/… с точки зрения реализации конечных автоматов.
Но это уже не является темой данной статьи.


UPD. Ладно, насчёт перечислений в Расте я не совсем прав, потому что хранение данных внутри перечисления упрощает моделирование. Пример можно посмотреть здесь. Но, опять же, цель этой статьи в том, чтобы показать, как реализовывать паттерн State в Rust, а не как писать State Machines на Rust.

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

Окей, тогда в последующих статьях буду искать примеры получше + мотивировать выбор паттерна, спасибо за совет!

Отличие в том что в плюсах нативное опп а variant тип выглядит "сложно". В Расте перечисление и матчинг по ним выглядит нативно а ООП наоборот не так тривиально.


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

Окей, тогда в последующих статьях буду искать примеры получше + мотивировать выбор паттерна, спасибо за совет!


Upd. Ответил немного не туда, но вам тоже спасибо! За столь долгое и продуктивное участие в дискуссии.

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