Rustのファインステートマシン

Andrew Hobdenの記事「Rustのプリティステートマシンパターン」に翻訳されています。 最後にオリジナルにリンクします。







最近、私はデザインパターンとプログラミングで使用する手法について多くのことを考えてきました。 プロジェクトの調査を開始し、既に見たことがあるおなじみのパターンやスタイルを見るのは本当に素晴らしいことです。 これにより、プロジェクトの理解が容易になり、作業をスピードアップできます。







新しいプロジェクトに取り組み、以前のプロジェクトと同じ方法で何かをする必要があることに気付くことがあります。 機能やライブラリの一部ではなく、エレガントなマクロや小さなコンテナでラップできないものである可能性があります。 問題をうまく解決するのは、単なる設計パターンまたは構造概念です。







このような問題によく適用される興味深いパターンの1つは、ステートマシンです。 このフレーズが何を意味するのか、そしてなぜそれがとても興味深いのかを理解するために少し時間をかけることをお勧めします。







記事全体を通して、 Rust Playgroundですべての例を実行できます。私は通常、Nightlyバージョンを習慣的に使用しています。







アイデアを正当化する



インターネット上のステートマシンに関する多くのリソースと機能の記事があります。 さらに、それらの多くの実現があります。







このページにアクセスするためだけにそれらの1つを使用しました。 状態マシンを使用してTCPプロトコルをシミュレートできます。 また、それを使用してHTTP要求をシミュレートすることもできます。 正規表現言語(REGEX)など、任意の正規言語を状態マシンとしてモデル化できます。 それらはどこにでもあり、私たちが毎日使うものの中に隠されています。







したがって、ステートマシンとは、それらの間に「状態」「遷移」のセットを持つ「マシン」です。







オートマトンについて話すとき、私たちは何かをする抽象的な概念を意味します 。 たとえば、Hello World!関数 -自動。 オンになり、最終的には期待どおりの結果が得られます。 データベースと対話するモデルも動作します。 基本的なオートマトンは、作成および破棄できる通常の構造と見なします。







struct Machine; fn main() { let my_machine = Machine; // . // `my_machine`  ,      . }
      
      





状態は、プロセスが状態マシンのどこにあるかを説明する方法です。 たとえば、ボトルを充填する機械を想像できます。 このマシンは、新しいボトルを待っているときは「待機」状態です。 彼がボトルを発見するとすぐに、彼は「満杯」状態になります。 ボトルに適切な量の水を満たした直後に、機械は「完了」状態になります。 彼はボトルが取られるとすぐに「待機」状態に戻ります。







これによる主な結論は、他の州に関連する情報を持っている州はないということです。 「充填」状態は、マシンが「スタンバイ」状態になっていた時間を気にしません。 「完了」のステータスは、ボトルの充満度を気にしません。 各州には、 責任と問題厳密に定義されています 。 これらのオプションを見る自然な方法はenum



です。







 enum BottleFillerState { Waiting { waiting_time: std::time::Duration }, Filling { rate: usize }, Done } struct BottleFiller { state: BottleFillerState }
      
      





この方法でenum



を使用することは、状態が相互に排他的であることを意味し、特定の時点では1つの状態にしかなれません。 Rustの「脂肪列挙」により、各状態に必要な情報を保存できます。 この方法で定義が宣言されている限り、すべてが順序どおりです。







しかし、小さな問題が1つあります。 上記のオートマトンを説明したときに、状態間の3つの遷移、







および







または



を考慮しませんでしたが、それらは単に意味をなしません!







これにより、遷移のアイデアがもたらされます。 真のステートマシンの最も快適な機能の1つは、 ->



などの遷移を心配する必要がないことです。 有限状態マシンの設計パターンは、このような移行が不可能であることを保証する必要があります。 理想的には、これはマシンを起動する前であっても、プログラムのコンパイル時に起こります。







ダイアグラム内の遷移をもう一度見てみましょう。







  +----------+ +------------+ +-----------+ | | | | | | |  +-->+  +-->+  | | | | | | | +----+-----+ +------------+ +--+--------+ ^ | +-----------------------------+
      
      





ご覧のとおり、状態の有限数と状態間の遷移の有限数があります。 現時点では、各状態間で完全に合法的に他の状態に移行できますが、ほとんどの場合、これは正しくありません。







これは、保留状態と充填状態の間の遷移が特定のルールを満たさなければならないことを意味します。 この例では、このルールは「ボトルは所定の位置にあります」のように見えます。 TCPストリームの場合、これは「FINパケットを受信しました」になります。つまり、ストリームを閉じて転送を完了する必要があります。







欲しいものを決める



ステートマシンが何であるかがわかったので、Rustでどのように実装しますか? 最初に、何が欲しいか考えてみましょう。







理想的には、次の特性を見たいと思います。









したがって、これらのすべての要件を満たすテンプレートがあれば、それは本当に素晴らしいでしょう。 まあ、要件の一部にのみ適しているテンプレートも良いでしょう。







可能な実装を探る



Rustのような強力で柔軟な型システムでは、これを実装できる必要があります。 真実はこれです。いくつかの方法があり、それぞれが特定の利点を提供し、レッスンを教えてくれます。







Enumを使用した2回目の試行



既に知っているように、 enum



最も自然な方法ですが、この場合、遷移を禁止できないことにすでに気付いています。 しかし、それらを構造でラップすることはできますか? もちろんできます! ご覧ください:







 enum State { Waiting { waiting_time: std::time::Duration }, Filling { rate: usize }, Done } struct StateMachine { state: State } impl StateMachine { fn new() -> Self { StateMachine { state: State::Waiting { waiting_time: std::time::Duration::new(0, 0)} } } fn to_filling(&mut self) { self.state = match self.state { //   "" -> ""  State::Waiting { .. } => State::Filling { rate: 1}, //    _ => panic!("Invalid state transition!") } } // ... } fn main() { let mut state_machine = StateMachine::new(); state_machine.to_filling(); }
      
      





一見、すべてが正常です。 しかし、いくつかの問題に気づきましたか?









ただし、このアプローチにはいくつかの利点があります。









「すみません、ホバーベア、 to_filling()



出力をResult<T,E>



ラップするか、 InvalidState



オプションをenum



追加できます!」 しかし、それに直面してみましょう:それは、状況をあまり改善しません。 実行時のクラッシュをなくしても、厄介なパターンマッチング式を処理する必要があり、プログラムの起動後にのみエラーが検出されます。 ふう! もっとうまくやれると約束します。







それでは、検索を続けましょう!







遷移を持つ構造



構造体のセットを使用する場合はどうなりますか? それらのそれぞれに対して、各状態に共通する特性のセットを定義できます。 あるタイプを別のタイプに変える特別な関数を使用できます! それはどのように見えますか?







 //      trait SharedFunctionality { fn get_shared_value(&self) -> usize; } struct Waiting { waiting_time: std::time::Duration, // ,     shared_value: usize } impl Waiting { fn new() -> Self { Waiting { waiting_time: std::time::Duration::new(0,0), shared_value: 0 } } //  ! fn to_filling(self) -> Filling { Filling { rate: 1, shared_value: 0 } } } impl SharedFunctionality for Waiting { fn get_shared_value(&self) -> usize { self.shared_value } } struct Filling { rate: usize, //      shared_value: usize, } impl SharedFunctionality for Filling { fn get_shared_value(&self) -> usize { self.shared_value } } // ... fn main() { let in_waiting_state = Waiting::new(); let in_filling_state = in_waiting_state.to_filling(); }
      
      





まあ、どのくらいのコード! したがって、アイデアは、すべての州が独自のデータだけでなく、すべての州に共通のデータを持っているというものでした。 ご覧のとおり、 to_filling()



関数to_filling()



「Waiting」状態to_filling()



吸収し、「Filling」状態に移行します。 すべてをまとめましょう:









いくつかの欠点があります。









 enum State { Waiting(Waiting), Filling(Filling), Done(Done) } fn main() { let in_waiting_state = State::Waiting(Waiting::new()); //    ,   `Waiting`   `enum`! //    `match`    let in_filling_state = State::Filling(in_waiting_state.to_filling()); }
      
      





ご覧のとおり、これはあまり便利ではありません。 私たちは私たちが望むものに近づいています。 特定のタイプを切り替えるというアイデアは、大きな前進のようです! 完全に異なるものを試す前に、サンプルを変更する方法について説明しましょう。







Rust標準ライブラリは、2つの非常に重要な特性、 From



Into



提供します。これらは非常に便利で言及する価値があります。 どちらか一方を実装すると自動的に他方が実装されることに注意することが重要です。 一般に、 From



実装がより柔軟であるため、望ましいです。 前の例では、これらを非常に簡単に実装できます。







 // ... impl From<Waiting> for Filling { fn from(val: Waiting) -> Filling { Filling { rate: 1, shared_value: val.shared_value, } } } // ...
      
      





これは、一般的な遷移関数を提供するだけでなく、ソースコードでこれに遭遇したときに読みやすくなります。 これにより、心理的な負担が軽減され、読者の認識が容易になります。 独自の 機能 を実装する代わりに 、既存のテンプレートを使用します。 既存のテンプレートに基づいたテンプレートのベースは、優れたソリューションです。







だからこれはクールです、私たちはどのように迷惑なコードの繰り返しとshared_value



どこで処理しますか? さらに探検しましょう!







ほぼ完璧



次に、最初の2つの方法からのレッスンとアイデアをまとめ、新しいアイデアを追加して、より楽しいものを取得します。 この方法の本質は、一般化された型の力を使用することです。 かなり基本的な構造を見てみましょう。







 struct BottleFillingMachine<S> { shared_value: usize, state: S } //     `S`  StateMachine<S> struct Waiting { waiting_time: std::time::Duration } struct Filling { rate: usize } struct Done;
      
      





そのため、実際には、 BottleFillingMachine



シグネチャにステートマシンの状態を埋め込みます。 Fill状態のステートマシンはBottleStateMachine<Filling>



になります。これは、このタイプをエラーメッセージまたはそのようなものの一部として見ると、すぐにマシンの現在の状態がわかるためです。







次のような特定のオプションに対して、 From<T>



を継続して実装できます。







 impl From<BottleFillingMachine<Waiting>> for BottleFillingMachine<Filling> { fn from(val: BottleFillingMachine<Waiting>) -> BottleFillingMachine<Filling> { BottleFillingMachine { shared_value: val.shared_value, state: Filling { rate: 1 } } } } impl From<BottleFillingMachine<Filling>> for BottleFillingMachine<Done> { fn from(val: BottleFillingMachine<Filling>) -> BottleFillingMachine<Done> { BottleFillingMachine { shared_value: val.shared_value, state: Done } } }
      
      





オートマトンの初期状態の定義は次のようになります。







 impl BottleFillingMachine<Waiting> { fn new(shared_value: usize) -> Self { BottleFillingMachine { shared_value: shared_value, state: Waiting { waiting_time: std::time::Duration::new(0, 0) } } } }
      
      





しかし、状態の変化はどのように見えますか? このように:







 fn main() { let in_waiting = BottleFillingMachine::<Waiting>::new(0); let in_filling = BottleFillingMachine::<Filling>::from(in_waiting); }
      
      





署名が出力タイプを制限する関数内でこれを行う場合:







 fn transition_the_states(val: BottleFillingMachine<Waiting>) -> BottleFillingMachine<Filling> { val.into() // ,   ? }
      
      





コンパイル時のエラーメッセージのタイプはどうですか?







 error[E0277]: the trait bound `BottleFillingMachine<Done>: std::convert::From<BottleFillingMachine<Waiting>>` is not satisfied --> <anon>:50:22 | 50 | let in_filling = BottleFillingMachine::<Done>::from(in_waiting); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: the following implementations were found: = help: <BottleFillingMachine<Filling> as std::convert::From<BottleFillingMachine<Waiting>>> = help: <BottleFillingMachine<Done> as std::convert::From<BottleFillingMachine<Filling>>> = note: required by `std::convert::From::from`
      
      





ここで何が間違っているかは明らかです。 エラーメッセージは、適切な遷移を示しています。







それで、このアプローチは私たちに何を与えますか?









まだ欠点があります:









ここでこの例を試してみることができます。







親との汚い関係



翻訳者注:このタイトルの翻訳は、Google Translatorが親切に提供してくれたのでとても素晴らしいので、そのままにしておきました。







ひどい相互作用の問題なしに、ステートマシンの状態を保存するための親構造をどのように整理できますか? まあ、それはenum



最初のアイデアに戻ります。







覚えている場合、最初のアプローチの主な問題は、トランジションを提供する機能がなく、実行時にすべてのエラーが表示されることでした。







 enum BottleFillingMachineWrapper { Waiting(BottleFillingMachine<Waiting>), Filling(BottleFillingMachine<Filling>), Done(BottleFillingMachine<Done>) } struct Factory { bottle_filling_machine: BottleFillingMachineWrapper } impl Factory { fn new() -> Self { Factory { bottle_filling_machine: BottleFillingMachineWrapper::Waiting(BottleFillingMachine::new(0)) } } }
      
      





この時点で、あなたの最初の反応は、「地獄、ホバーベア、これらの長くひどい型宣言を見てください」です。 あなたは絶対に正しいです! 正直なところ、それらは本当に長いですが、私は最も理解しやすいタイプ名を選びました! コードでは、お気に入りの略語とエイリアスをすべて使用できます。

見て!







 impl BottleFillingMachineWrapper { fn step(&mut self) -> Self { match self { BottleFillingMachineWrapper::Waiting(val) => BottleFillingMachineWrapper::Filling(val.into()), BottleFillingMachineWrapper::Filling(val) => BottleFillingMachineWrapper::Done(val.into()), BottleFillingMachineWrapper::Done(val) => BottleFillingMachineWrapper::Waiting(val.into()) } } } fn main() { let mut the_factory = Factory::new(); the_factory.bottle_filling_machine = the_factory.bottle_filling_machine.step(); }
      
      





繰り返しますが、これは変化ではなく吸収によって機能することに気付くでしょう。 match



を使用してval



移動し.into()



それ.into()



使用して以前の状態を吸収できるように.into()



ます。 ただし、値を変更する場合は、 #[derive(Clone)]



実装#[derive(Clone)]



か、状態に合わせてCopy



#[derive(Clone)]



こともできます。







仕事には多少不便で快適ですが、型のシステムとそれに付随するすべての保証を備えたトランジションがまだあります。







このメソッドを使用すると、ステートマシンでの操作中に考えられるすべての状態を強制的に処理することに気付く場合がありますが、これは理にかなっています。 ステートマシンを使用して構造を所有および管理する場合、ステートマシンが存在できる各ステートのアクションを決定する必要があります。







または、単にpanic!()



呼び出すことができますpanic!()



本当にしたい場合。 しかし、単にpanic



たい場合は、最初のアプローチを使用しないのはなぜですか?







ここで完全に機能する例を見ることができます。







実施例



これは、例が不要でない場合に当てはまります。 そこで、以下にいくつかの実例を作成し、コメントを提供しました。







3つの状態、2つの遷移



この例は、ボトル充填機に非常に似ていますが、非常に些細ではありますが、 実際に機能します。 このステートマシンは文字列を受け取り、その中の単語数を返します。

Rust Playgroundへのリンク







 fn main() { //   <StateA>.       ! let in_state_a = StateMachine::new("  ".into()); //   ,         in_state_a.some_unrelated_value; println!(" : {}", in_state_a.state.start_value); //    .    //      let in_state_b = StateMachine::<StateB>::from(in_state_a); //   !     ! // in_state_a.some_unrelated_value; //           in_state_b.some_unrelated_value; println!(" : {:?}", in_state_b.state.interm_value); //     let in_state_c = StateMachine::<StateC>::from(in_state_b); //     !      ! // in_state_c.state.start_value; println!(" : {}", in_state_c.state.final_value); } //     struct StateMachine<S> { some_unrelated_value: usize, state: S } //       impl StateMachine<StateA> { fn new(val: String) -> Self { StateMachine { some_unrelated_value: 0, state: StateA::new(val) } } } //        struct StateA { start_value: String } impl StateA { fn new(start_value: String) -> Self { StateA { start_value: start_value, } } } //  B     struct StateB { interm_value: Vec<String>, } impl From<StateMachine<StateA>> for StateMachine<StateB> { fn from(val: StateMachine<StateA>) -> StateMachine<StateB> { StateMachine { some_unrelated_value: val.some_unrelated_value, state: StateB { interm_value: val.state.start_value.split(" ").map(|x| x.into()).collect(), } } } } // ,      ,       struct StateC { final_value: usize, } impl From<StateMachine<StateB>> for StateMachine<StateC> { fn from(val: StateMachine<StateB>) -> StateMachine<StateC> { StateMachine { some_unrelated_value: val.some_unrelated_value, state: StateC { final_value: val.state.interm_value.len(), } } } }
      
      





いかだ



最近私のブログ投稿をフォローしているなら、おそらくRaftについて書くことを好むことをご存知でしょう。 この研究を実施するきっかけとなったのは、Raftと@argorakとの会話でした







状態間の遷移はA->B->C



ように線形ではないため、いかだは前の例よりもやや複雑ですA->B->C



この状態マシンの状態図と遷移を以下に示します。







 +----------+ +-----------+ +--------+ | +----> | | | | Follower | | Candidate +----> Leader | | <----+ | | | +--------^-+ +-----------+ +-+------+ | | +-------------------------+
      
      





Rust Playgroundへのリンク







 //      fn main() { let is_follower = Raft::new(/* ... */); //  ,  3, 5  7  Raft.      :) //      let is_candidate = Raft::<Candidate>::from(is_follower); //  !   let is_leader = Raft::<Leader>::from(is_candidate); //        Follower let is_follower_again = Raft::<Follower>::from(is_leader); //    ... let is_candidate_again = Raft::<Candidate>::from(is_follower_again); //     ! let is_follower_another_time = Raft::<Follower>::from(is_candidate_again); } //     struct Raft<S> { // ...   state: S } //  ,      Raft //     ,     struct Leader { // ...    } //   ,            struct Candidate { // ...    } //    ,   struct Follower { // ...    } // Raft    Follower impl Raft<Follower> { fn new(/* ... */) -> Self { // ... Raft { // ... state: Follower { /* ... */ } } } } //       //     ,     impl From<Raft<Follower>> for Raft<Candidate> { fn from(val: Raft<Follower>) -> Raft<Candidate> { // ...      Raft { // ... attr: val.attr state: Candidate { /* ... */ } } } } //       ,      impl From<Raft<Candidate>> for Raft<Follower> { fn from(val: Raft<Candidate>) -> Raft<Follower> { // ...      Raft { // ... attr: val.attr state: Follower { /* ... */ } } } } //       impl From<Raft<Candidate>> for Raft<Leader> { fn from(val: Raft<Candidate>) -> Raft<Leader> { // ...      Raft { // ... attr: val.attr state: Leader { /* ... */ } } } } //   ,     ,    impl From<Raft<Leader>> for Raft<Follower> { fn from(val: Raft<Leader>) -> Raft<Follower> { // ...      Raft { // ... attr: val.attr state: Follower { /* ... */ } } } }
      
      







I-impv Reddit , , . :







. -.

, :

  • . (, ), .
  • "", .


!









Rust . enum



, . , , .







, . IRC Mozilla Hoverbear.







:

: Andrew Hobden








All Articles