Реализация конечных автоматов через систему типов Rust
При проектировании конечных автоматов в 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,
}