RuStateとは
RuStateは、Rustで実装された状態機械(ステートマシン)ライブラリで、WebAssemblyを通じてブラウザからの利用を可能にします。
状態機械は、プログラムが取りうる有限の状態と、その間の遷移を明示的に定義することで、複雑な振る舞いを管理するパターンです。
RuStateは特に以下のような場面で威力を発揮します:
- 複雑なUIの状態遷移
- バリデーションを含むフォーム管理
- ワークフローやプロセス制御
- ゲームのロジック実装
- 安全性が求められるビジネスルールの実装
主な特徴
- 型安全な状態管理: Rustの強力な型システムを活用した安全な状態遷移
- WebAssembly対応: wasmを通じてJavaScriptから利用可能
- 軽量設計: 必要最小限の依存関係と高速な実行
- 自由度の高いAPI: さまざまなユースケースに適応可能
基本的な使い方
1. ステートマシンの定義
Rustで状態と遷移を定義します:
// Rust側のコード
pub enum TrafficLightState {
Red,
Yellow,
Green,
}
pub enum TrafficLightEvent {
CHANGE,
RESET,
}
impl StateMachine for TrafficLight {
fn transition(&mut self, event: TrafficLightEvent) {
self.state = match (&self.state, event) {
(TrafficLightState::Red, TrafficLightEvent::CHANGE) => TrafficLightState::Green,
(TrafficLightState::Green, TrafficLightEvent::CHANGE) => TrafficLightState::Yellow,
(TrafficLightState::Yellow, TrafficLightEvent::CHANGE) => TrafficLightState::Red,
(_, TrafficLightEvent::RESET) => TrafficLightState::Red,
};
}
}
2. WebAssemblyへのエクスポート
wasm-bindgenを使ってJavaScript向けにサービスを公開します:
#[wasm_bindgen]
pub struct TrafficLightService {
machine: TrafficLight,
}
#[wasm_bindgen]
impl TrafficLightService {
pub fn new() -> Self {
TrafficLightService {
machine: TrafficLight::new(),
}
}
pub fn send(&mut self, event: &str) {
let event = match event {
"CHANGE" => TrafficLightEvent::CHANGE,
"RESET" => TrafficLightEvent::RESET,
_ => return,
};
self.machine.transition(event);
}
pub fn get_state(&self) -> String {
format!("{:?}", self.machine.state)
}
}
3. JavaScript側での利用
ブラウザで以下のようにして利用します:
// WebAssemblyモジュールをロード
wasm_demo.default().then(() => {
// TrafficLightサービスを初期化
const trafficLightService = wasm_demo.createTrafficLightService();
// イベントの送信
trafficLightService.send('CHANGE');
// 現在の状態の取得
const currentState = trafficLightService.getState();
});
高度な使い方
コンテキスト付きの状態機械
状態だけでなく、追加のデータを持つステートマシンを定義できます:
pub struct CounterMachine {
state: CounterState,
context: CounterContext,
}
pub struct CounterContext {
count: i32,
max_value: i32,
}
// 状態遷移時にコンテキストも更新
impl StateMachine for CounterMachine {
fn transition(&mut self, event: CounterEvent) {
match (&self.state, event) {
(CounterState::Active, CounterEvent::Increment) => {
if self.context.count < self.context.max_value {
self.context.count += 1;
} else {
self.state = CounterState::MaxReached;
}
},
// 他の遷移...
}
}
}
条件付き遷移
遷移にガード条件を設定する例:
fn transition(&mut self, event: UserEvent) {
self.state = match (&self.state, event) {
(UserState::LoggedOut, UserEvent::Login) => {
if self.validate_credentials() {
UserState::LoggedIn
} else {
UserState::Error
}
},
// 他の遷移...
};
}
API リファレンス
StateMachine トレイト
すべての状態機械の基本となるトレイトです:
pub trait StateMachine {
type State;
type Event;
fn transition(&mut self, event: Self::Event);
fn get_state(&self) -> &Self::State;
fn can_transition(&self, event: &Self::Event) -> bool;
}
Serviceトレイト
WebAssemblyとして公開するための標準的なインターフェース:
pub trait Service {
fn send(&mut self, event: &str) -> Result<(), JsValue>;
fn get_state(&self) -> JsValue;
fn get_meta(&self) -> JsValue;
}
コアAPI関数一覧
関数名 | 説明 | 引数 | 戻り値 |
---|---|---|---|
transition | 指定されたイベントで状態遷移を実行 | event: Event | なし |
get_state | 現在の状態を取得 | なし | State |
can_transition | 特定のイベントで遷移可能か検証 | event: &Event | bool |
send | 文字列イベントを送信(JS連携用) | event: &str | Result |
get_context | コンテキストデータを取得 | なし | Context |
get_meta | 状態機械のメタデータを取得 | なし | JsValue |
エラー処理
RuStateでは以下のような方法でエラーを扱います:
// Rust側
pub enum StateMachineError {
InvalidTransition,
InvalidState,
InvalidEvent,
// その他のエラー
}
// 結果をResultで返す
pub fn send(&mut self, event: &str) -> Result<(), StateMachineError> {
let parsed_event = self.parse_event(event)?;
if !self.can_transition(&parsed_event) {
return Err(StateMachineError::InvalidTransition);
}
self.transition(parsed_event);
Ok(())
}
WebAssembly 連携詳細
wasm-bindgenの使い方
Rustコードをwasmにコンパイルし、JavaScriptから利用可能にするための設定:
// Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Oz"]
型変換
RustとJavaScript間の型変換方法:
// 文字列の変換
#[wasm_bindgen]
pub fn to_uppercase(input: &str) -> String {
input.to_uppercase()
}
// 複合データ型の変換
#[wasm_bindgen]
pub fn get_machine_state(&self) -> JsValue {
let state = self.machine.get_state();
let context = self.machine.get_context();
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"state".into(), &state.to_string().into()).unwrap();
js_sys::Reflect::set(&obj, &"count".into(), &context.count.into()).unwrap();
obj.into()
}
非同期処理
WebAssembly内での非同期処理の扱い方:
#[wasm_bindgen]
pub async fn async_operation() -> Result {
// 非同期処理の実装
let result = perform_async_task().await?;
Ok(result.into())
}
// JavaScript側での利用
machine.asyncOperation().then(result => {
console.log("Operation completed:", result);
});
メモリ管理の最適化
WebAssemblyとJavaScript間のメモリ共有と最適化テクニック:
- 大きなデータ構造はJavaScript側で管理し、必要な部分のみwasmに渡す
- 頻繁に変更されるデータは適切にキャッシュする
- 大量のデータ転送が必要な場合はArrayBufferを使用する
実装パターン集
複数の状態機械の連携
大規模アプリケーションでは、複数の小さな状態機械を組み合わせることが効果的です:
pub struct AppService {
auth_machine: AuthMachine,
form_machine: FormMachine,
navigation_machine: NavigationMachine,
}
#[wasm_bindgen]
impl AppService {
pub fn send_to_auth(&mut self, event: &str) -> Result<(), JsValue> {
self.auth_machine.send(event)
}
pub fn send_to_form(&mut self, event: &str) -> Result<(), JsValue> {
self.form_machine.send(event)
}
// イベントに応じて適切な状態機械にルーティング
pub fn send(&mut self, target: &str, event: &str) -> Result<(), JsValue> {
match target {
"auth" => self.send_to_auth(event),
"form" => self.send_to_form(event),
// その他...
_ => Err("Invalid target".into()),
}
}
}
Reactとの連携パターン
React.jsでRuStateを利用する効果的な方法:
// React Custom Hook
function useStateMachine(initialMachine) {
const [state, setState] = useState(initialMachine.getState());
const machineRef = useRef(initialMachine);
const send = useCallback((event) => {
machineRef.current.send(event);
setState(machineRef.current.getState());
}, []);
return [state, send];
}
// コンポーネント内での利用
function TrafficLight() {
const [machine, setMachine] = useState(null);
const [state, send] = useStateMachine(machine);
useEffect(() => {
wasm_module.default().then(() => {
setMachine(wasm_module.createTrafficLightMachine());
});
}, []);
if (!machine) return Loading...;
return (
);
}
状態の永続化
ステートマシンの状態を保存・復元する方法:
#[wasm_bindgen]
impl MachineService {
// 状態のシリアライズ
pub fn serialize(&self) -> String {
serde_json::to_string(&self.machine).unwrap_or_default()
}
// 状態の復元
pub fn deserialize(data: &str) -> Result {
let machine: Machine = serde_json::from_str(data)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(MachineService { machine })
}
}
// JavaScript側での利用
// 状態の保存
localStorage.setItem('machineState', machine.serialize());
// 状態の復元
const savedState = localStorage.getItem('machineState');
if (savedState) {
const machine = MachineService.deserialize(savedState);
}
プロジェクト構成
RuStateを使ったプロジェクトの基本構成:
rustate/
├── Cargo.toml
├── src/
│ ├── lib.rs # メインライブラリコード
│ ├── machines/ # 状態機械の実装
│ └── bindings.rs # WebAssembly向けバインディング
└── web/
├── index.html # デモページ
├── docs.html # このドキュメント
├── style.css # スタイルシート
└── pkg/ # ビルドされたwasmモジュール
ビルドとデプロイ
環境設定
必要なツール:
- Rust と Cargo
- wasm-pack
- Node.js (オプション)
ビルド手順
# WebAssemblyとしてビルド
wasm-pack build --target web
# 開発サーバーを起動(オプション)
npx serve .
CI/CDパイプラインの例
GitHub Actionsを使った継続的インテグレーション・デプロイメントの設定例:
name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build
run: wasm-pack build --target web
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./web
パフォーマンスとデバッグ
パフォーマンス考慮事項
RuStateで効率的なアプリケーションを構築するためのヒント:
- 状態機械は小さく保ち、責任を明確に分離する
- 頻繁に更新される大きなデータ構造は状態から分離する
- デバッグビルドとリリースビルドでパフォーマンスが大きく異なることを認識する
- メモリ使用量を監視し、不必要なクローンや変換を避ける
- 重い処理は Web Workers に委譲することを検討する
デバッグツール
wasm-pack testコマンドを使用して、WebAssemblyコードのテストを実行できます。
開発時は、console.logでの状態出力や、chromeのWebAssemblyデバッグツールも活用してください。
状態遷移の可視化
状態遷移をログとして出力し、デバッグを容易にする方法:
#[wasm_bindgen]
impl MachineService {
pub fn send_with_logging(&mut self, event: &str) -> Result<(), JsValue> {
let prev_state = self.get_state();
let result = self.send(event);
if result.is_ok() {
let new_state = self.get_state();
web_sys::console::log_3(
&"Transition:".into(),
&format!("{} --[{}]--> {}", prev_state, event, new_state).into(),
&self.get_context().into()
);
}
result
}
}
よくある質問
- RuStateは他のステートマシンライブラリとどう違いますか?
- RuStateは特にWebAssemblyとの連携に重点を置いており、Rustの型安全性を最大限に活用しています。
- 大規模なアプリケーションでも利用できますか?
- はい。複数の小さな状態機械を組み合わせることで、大規模なアプリケーションも構築できます。
- Redux/XStateなどのJavaScriptライブラリと併用できますか?
- はい。WebAssemblyを通じて公開されるAPIを利用して、既存のJavaScriptフレームワークと統合できます。
- パフォーマンスはJavaScript実装と比べてどうですか?
- 一般的に、計算量の多い処理はWebAssembly版の方が高速ですが、DOM操作などJavaScriptとの頻繁なやり取りが必要な場合は、オーバーヘッドにより差が縮まることがあります。
- どのようなユースケースに最適ですか?
- 複雑なビジネスロジック、計算集約型の処理、厳格な型安全性が求められるアプリケーションに最適です。特に、ユーザーインターフェース、ワークフロー管理、ゲームロジックなどに適しています。
- テスト方法はどうすればよいですか?
- Rustの単体テストとwasm-packのテスト機能を組み合わせることで、効率的にテストできます。状態機械の各遷移を個別にテストし、エッジケースも確認することをお勧めします。