При проектировании конечных автоматов в 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,
}

Комментарии (10)


  1. thegriglat
    00.00.0000 00:00
    +1

    я начинающий rustacean, не лучше ли реализовать трейт Into<FooReady>?

        impl Into<FooReady> for FooInit {
            fn into(self) -> FooReady {
                FooReady { c: self.a + self.b }
            }
        }
    // ...
        let foo: FooReady = foo_system::FooInit::new()
            // Подготавливаем данные в поле 'a'
            .set_a(1)
            // Подготавливаем данные в поле 'b'
            .set_b(2)
            .into();


    1. PROgrammer_JARvis
      00.00.0000 00:00
      +4

      Да, для конверсий стоит использовать именно трейты From и Into.

      Единственное, что из этой пары следует реализовывать именно From, поскольку Into в таком случае реализуется автоматически (обратное неверно), то есть:

      impl From<FooInit> for FooReady {
          fn from(init: FooInit) -> Self {
              Self { c: init.a + init.b }
          }
      }
      

      Также стоит обратить внимание на трейты TryFrom и TryInto, которые также используются для конверсий, но на этот раз таких, которые могут быть неуспешными (они, кстати, также автоматически реализуются с типом невозможной ошибки Infallible, если реализованы обычные конверсии From).


      1. nizovtsevnv Автор
        00.00.0000 00:00

        Спасибо! Использование трэйтов это немного глубже, хотя и интереснее. Статья рассчитана на начинающих, переходящих с языков без жёсткой типизации для пояснения использования отдельных типов для хранения состояний и переходов между ними с использованием методов. Статья ни в коем случае не претендует на полноту изложения и идеальную полномасштабную реализацию . Концептуально она отражает расширенный вариант документации.


  1. tessob
    00.00.0000 00:00
    +9

    Не понятно как это все вообще относится к конечным автоматам. Где множество состояний и грамматика/алфавит? Ещё понятно, когда на Rust в качестве примера приводят пару enum или enum и строку, но это, простите, что?


    1. nizovtsevnv Автор
      00.00.0000 00:00

      Понимаю праведный гнев математика, однако: терминами алфавита и грамматики пользуются хоть и часто, но это не является единственным описанием и типом применения конечных автоматов. Речь не о математике, а о применении индивидуальных типов как хранилища состояния и поведения системы в конкретном языке программирования вместо Enum или любых других элементарных типов (строк, чисел, булевых значений).

      К конечным автоматам описание имеет следующее отношение:

      * конечный автомат это модель системы имеющий один вход и один выход

      * в каждый момент времени находящийся в одном состоянии - реализуется типом FooInit / FooReady

      * на вход системы поступают входные воздействия - вызов методов

      * на выходе системы формируются выходные сигналы - результаты исполнения этих методов


      1. Dr_Dash
        00.00.0000 00:00

        Конечный автомат это система имеющая внутренее состояние, и произвольное (но определённое) количество входов и выходов. Почитайте по ссылке

        Автоматное программирование – новая веха или миф? Часть 1. Введение https://habr.com/p/331556/


  1. Dark_Furia
    00.00.0000 00:00
    +1

    Особо заинтересованным в теме стоит ознакомиться с этим постом. Работа с конечными автоматами рассмотрена куда глубже


    1. nizovtsevnv Автор
      00.00.0000 00:00

      Спасибо за полезную ссылку. Побольше бы статей на русском языке.


      1. Dark_Furia
        00.00.0000 00:00

        Ее уже перевели кстати


      1. dprotopopov
        00.00.0000 00:00

        Конечные автоматы не такаю уж и сложная тема (если не лезть в дебри типа композиций автоматов, построения автоматов по данным ввода-вывода и доказывания эквивалентности)

        Вики вам в помощь https://ru.wikipedia.org/wiki/Конечный_автомат