При проектировании конечных автоматов в Rust хранение информации о текущем состоянии системы очень часто производится в объекте типа соответствующего его состоянию. При этом изменение состояния системы вызывает создание экземпляра другого типа (соответствующего её состоянию).
Выбор такого подхода в Rust связан со следующими особенностями:
Rust использует концепцию анализа Typestate
Используется строгая типизация данных, нет средств для автоматического создания и хранения объектов без инициализации всех значений для описанных полей данных. Для инициализации “сложных” объектов часто применяется шаблон проектирования Строитель (Builder)
Rust не предоставляет средств для автоматической трансформации типов. Для перевода экземпляра данных одного типа в другой используется вызов соответствующих методов типа (например
into_f32()
)Язык Rust, избегая состояния гонки данных, повсеместно использует понятия владения и заимствования. Для перевода экземпляра данных одного типа в другой данные либо меняют владельца (и не доступны в первичном экземпляре после этого), либо в памяти должна быть размещена копия этих данных. Заимствование значения из исходного экземпляра в данном случае создаёт больше проблем.
Простейший пример построения конечного автомата имеющего два состояния:
состояние с не подготовленными данными будем хранить в типе
FooInit
(реализует шаблон проектирования Строитель);состояние с данными готовыми к использованию будем хранить в типе
FooReady
.
Реализация шаблона с однократным использованием структуры данных FooInit
(созданием нового экземпляра на каждой итерации подготовки данных):
pub mod foo_system{
// Структура данных для работы с готовыми данными
#[derive(Debug)]
pub struct FooReady {
c: u32,
}
// Структура данных для подготовки данных по шаблону Строитель
pub struct FooInit {
a: u32,
b: u32,
}
// Реализация методов для шаблона Строитель
impl FooInit {
// Конструктор объекта FooInit с данными по умолчанию
pub fn new() -> Self {
Self {
a: 0,
b: 0,
}
}
// Подготовка данных в поле 'a' FooInit
pub fn set_a(self, a: u32) -> Self {
// Создаём новую структуру данных
Self {
a,
..self // заполняем другие поля из исходного экземпляра
}
}
// Подготовка данных в поле 'b' FooInit
pub fn set_b(self, b: u32) -> Self {
// Создаём новую структуру данных
Self {
b,
..self // заполняем другие поля из исходного экземпляра
}
}
// Смена состояния FooInit -> FooReady
pub fn into_foo(self) -> FooReady {
FooReady {
c: self.a + self.b,
}
}
}
}
fn main() {
// Создаём конечный автомат в состоянии FooInit
let foo = foo_system::FooInit::new()
// Подготавливаем данные в поле 'a'
.set_a(1)
// Подготавливаем данные в поле 'b'
.set_b(2)
// Переводим систему в состояние FooReady
.into_foo();
// Работаем с системой в состоянии FooReady
println!("{:#?}", foo);
}
Реализация шаблона с повторным использованием структуры данных FooInit
(изменением данных в исходной структуре на каждой итерации подготовки данных):
pub mod foo_system{
// Структура данных для работы с готовыми данными
#[derive(Debug)]
pub struct FooReady {
c: u32,
}
// Структура данных для подготовки данных по шаблону Строитель
pub struct FooInit {
a: u32,
b: u32,
}
// Реализация методов для шаблона Строитель
impl FooInit {
// Конструктор объекта FooInit с данными по умолчанию
pub fn new() -> Self {
Self {
a: 0,
b: 0,
}
}
// Подготовка данных в поле 'a' FooInit
pub fn set_a(&mut self, a: u32) -> &Self {
self.a = a; // меняем данные в структуре
self
}
// Подготовка данных в поле 'b' FooInit
pub fn set_b(&mut self, b: u32) -> &Self {
self.b = b; // меняем данные в структуре
self
}
// Смена состояния FooInit -> FooReady
pub fn into_foo(&self) -> FooReady {
FooReady {
c: self.a + self.b,
}
}
}
}
fn main() {
// Создаём конечный автомат в состоянии FooInit
let mut foo = foo_system::FooInit::new();
// Подготавливаем данные в поле 'a'
foo.set_a(1);
// Подготавливаем данные в поле 'b'
foo.set_b(2);
// Переводим систему в состояние FooReady
let foo = foo.into_foo();
// Работаем с системой в состоянии FooReady
println!("{:#?}", foo);
}
В обоих вариантах реализации результатом исполнения будет вывод в консоль:
FooReady {
c: 3,
}
Комментарии (10)
tessob
00.00.0000 00:00+9Не понятно как это все вообще относится к конечным автоматам. Где множество состояний и грамматика/алфавит? Ещё понятно, когда на Rust в качестве примера приводят пару enum или enum и строку, но это, простите, что?
nizovtsevnv Автор
00.00.0000 00:00Понимаю праведный гнев математика, однако: терминами алфавита и грамматики пользуются хоть и часто, но это не является единственным описанием и типом применения конечных автоматов. Речь не о математике, а о применении индивидуальных типов как хранилища состояния и поведения системы в конкретном языке программирования вместо Enum или любых других элементарных типов (строк, чисел, булевых значений).
К конечным автоматам описание имеет следующее отношение:
* конечный автомат это модель системы имеющий один вход и один выход
* в каждый момент времени находящийся в одном состоянии - реализуется типом FooInit / FooReady
* на вход системы поступают входные воздействия - вызов методов
* на выходе системы формируются выходные сигналы - результаты исполнения этих методов
Dr_Dash
00.00.0000 00:00Конечный автомат это система имеющая внутренее состояние, и произвольное (но определённое) количество входов и выходов. Почитайте по ссылке
Автоматное программирование – новая веха или миф? Часть 1. Введение https://habr.com/p/331556/
Dark_Furia
00.00.0000 00:00+1Особо заинтересованным в теме стоит ознакомиться с этим постом. Работа с конечными автоматами рассмотрена куда глубже
nizovtsevnv Автор
00.00.0000 00:00Спасибо за полезную ссылку. Побольше бы статей на русском языке.
dprotopopov
00.00.0000 00:00Конечные автоматы не такаю уж и сложная тема (если не лезть в дебри типа композиций автоматов, построения автоматов по данным ввода-вывода и доказывания эквивалентности)
Вики вам в помощь https://ru.wikipedia.org/wiki/Конечный_автомат
thegriglat
я начинающий rustacean, не лучше ли реализовать трейт Into<FooReady>?
PROgrammer_JARvis
Да, для конверсий стоит использовать именно трейты
From
иInto
.Единственное, что из этой пары следует реализовывать именно
From
, посколькуInto
в таком случае реализуется автоматически (обратное неверно), то есть:Также стоит обратить внимание на трейты
TryFrom
иTryInto
, которые также используются для конверсий, но на этот раз таких, которые могут быть неуспешными (они, кстати, также автоматически реализуются с типом невозможной ошибкиInfallible
, если реализованы обычные конверсииFrom
).nizovtsevnv Автор
Спасибо! Использование трэйтов это немного глубже, хотя и интереснее. Статья рассчитана на начинающих, переходящих с языков без жёсткой типизации для пояснения использования отдельных типов для хранения состояний и переходов между ними с использованием методов. Статья ни в коем случае не претендует на полноту изложения и идеальную полномасштабную реализацию . Концептуально она отражает расширенный вариант документации.