Реализация конечных автоматов через систему типов Rust

от автора

При проектировании конечных автоматов в Rust хранение информации о текущем состоянии системы очень часто производится в объекте типа соответствующего его состоянию. При этом изменение состояния системы вызывает создание экземпляра другого типа (соответствующего её состоянию).

Выбор такого подхода в Rust связан со следующими особенностями:

  • Rust использует концепцию анализа Typestate

  • Используется строгая типизация данных, нет средств для автоматического создания и хранения объектов без инициализации всех значений для описанных полей данных. Для инициализации “сложных” объектов часто применяется шаблон проектирования Строитель (Builder)

  • Rust не предоставляет средств для автоматической трансформации типов. Для перевода экземпляра данных одного типа в другой используется вызов соответствующих методов типа (например into_f32())

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


Простейший пример построения конечного автомата имеющего два состояния:

  1. состояние с не подготовленными данными будем хранить в типе FooInit (реализует шаблон проектирования Строитель);

  2. состояние с данными готовыми к использованию будем хранить в типе 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, }


ссылка на оригинал статьи https://habr.com/ru/post/725148/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *