Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Rust でステートマシンを扱えるライブラリを書いてみた

TL; DR

  • Rust でステートマシンをいい感じに扱えるようにするライブラリを書いてみた。
  • とくに他の言語のライブラリを調査したわけではないので、もう少しいいアイディアがあるかもしれない。

何を作ったか

Rust でマクロや他のライブラリを一切使わずにステートマシンを実装できるライブラリを作り、crates.io に公開してみました。

https://crates.io/crates/statemachine-rs

ドキュメントはこちらにあります。

docs.rs

リポジトリはこちらです。

github.com

何ができるのか

詳しくは README に記載しておりますので、使用してみたい場合はぜひご覧ください。

Rust には enum(Rust では代数的データ型になっています)という大変素晴らしい概念が利用できます。ステートマシンにもこれを利用しないわけにはいきませんよね。このライブラリは、enum 等を使用したステートマシンの実装のお手伝いをします。

下記のサンプルでは、ボタンの切り替えを行ってみています。

まず初期設定を StateMachineBuilder というビルダーを使って行えるように実装してあります。

State として ButtonState を用意し、Input として Input という enum を定義します。下記では On/Off という状態を、Press という入力を使って切り替えられるようにしています。

Input に対して State がどのように変化するのかは、クロージャーで定義します。transition 関数の引数は StateInput のタプルになっています。ここで、状態と入力のパターンマッチングを行い、状態遷移を定義します。

use statemachine_rs::machine::{builder::StateMachineBuilder, StateMachine};

#[derive(Clone, Debug, PartialEq)]
enum ButtonState {
    On,
    Off,
}

enum Input {
    Press,
}

fn main() {
    let sm = StateMachineBuilder::start()
        .initial_state(ButtonState::Off)
        .transition(|state, input| match (state, input) {
            (ButtonState::On, Input::Press) => ButtonState::Off,
            (ButtonState::Off, Input::Press) => ButtonState::On,
        })
        .build()
        .unwrap();

    assert_eq!(ButtonState::Off, sm.current_state());
    sm.consume(Input::Press);
    assert_eq!(ButtonState::On, sm.current_state());
}

状態を進める際には consume という関数が生えており、この consume に入力を渡すと状態遷移が行われます。consumeStateMachine トレイトの関数です。consume 以外にも、現在の状態の取得や状態のリセットなどを行える関数を用意してあります。また、StateMachine トレイトは pub なため、自身で構造体とその実装を定義してオリジナルのステートマシンを用意することもできます。

pub trait StateMachine<State, Input> {
    fn current_state(&self) -> State;

    fn consume(&self, input: Input) -> State;

    fn reset(&self) -> State;

    fn set(&self, new_state: State);
}

実装して迷っているポイント

今回 Rust ではじめて OSS のライブラリを作ってみたので、慣習がわかっていない部分があります。

実装の都合で Stateenum に対しては現在 Clone が要求されます。現在、標準で提供しているのは BasicStateMachine という構造体です。これは StateClone を要求しています。実装側を見ていただくとわかるのですが、どうしても clone が必要と思われる箇所が出てくるためです。

/// The basic state machine implementation.
/// It holds `initial_state`, `current_state`, `transition` function.
pub struct BasicStateMachine<State, Input, Transition>
where
    Transition: Fn(&State, Input) -> State,
    State: Clone,
{
    /// `initial_state` is literally an initial state of the state machine.
    /// The field isn't updated the whole life of its state machine.
    /// That is, it always returns its initial state of its machine.
    initial_state: State,
    /// `current_state` is the current state of the state machine.
    /// It transit to the next state via `transition`.
    current_state: RefCell<StateWrapper<State>>,
    /// `transition` is the definition of state transition.
    /// See an example of [`StateMachine::consume()`], you can grasp how
    /// to define the transition.
    transition: Transition,
    _maker: PhantomData<Input>,
}

もし StateClone を要求するのが、大きなパフォーマンス劣化を招く原因になりうるのだとしたら、この Clone の制約を外せるように実装を調整する必要があるのかなと思っています。が、正直これをどう上手に調整したらよいかの想像がついていません。このあたりのワークアラウンドは結構迷ってます…。

もし何かアイディアがある方がいらっしゃいましたら、お気軽に Issue を立てたり、PR を投げたりなどよろしくお願いします😃

また、BasicStateMachineRefCell を使用していることからもわかるようにスレッドセーフではありません。スレッドセーフな StateMachine 実装は、今後時間を見つけて追加する予定です。

はじめてのクレート公開の感想

cargo package からの cargo publish をするだけでクレートを公開できてしまうのは、正直かなり便利ですね。Scala のときは sonatype だったか、どこかの外部サービスに一度ユーザー登録をして、JIRA をうんたらかんたら…といった作業が必要だった(勘違いしてるかも)と記憶しているのですが、crates.io は GitHub の ID さえあればアカウントを作成でき、サイト上でアクセスキーを生成してあとはローカルから cargo publish を打つだけ、という非常にスムーズな構成になっていました。

OSS の公開に際しての悩みの種といえばドキュメントですが、ドキュメントは crates.io に登録した時点で自動的に URL を用意してくれ、そちらに cargo doc の結果をホストしてくれます。今回私が作ったクレートのドキュメントはこちらにありますが、これを見たときに「ああ、クレートを公開しちゃったよ…すごい…」という気持ちになりました。いたれりつくせり。

ドキュメントを書くのはかなり骨の折れる作業で、主にサンプルコードの用意の部分で結構手間取りました。ただ、サンプルコードを用意しておくと、cargo test したタイミングで doc test が走り、コメント内のコードのコンパイルが通るかや assertion が通るかを自動でチェックしてくれます。ドキュメントのメンテ漏れに気づけるようになるので、この仕組みは非常に素晴らしいです。ちなみに他の言語での経験だと Python でも同じことができますね。

クレートの使い勝手については、自分が普段 Rust コードを書く際には問題ないなというレベルで公開してしまっているため、他の方がどう感じるかが結構気になっています。というわけで、ドッグフーディングしてくださる方、よろしくお願いします。Issue や PR お待ちしております!