SMC(State Map Compiler) の拡張
はじめに
C Magazine 1999/9 で、SMC(State Map Compiler)を紹介しました。 SMCに状態遷移表を記述したスクリプト(テキストファイル)を食わせると、状態遷移表に書かれた通りに動作するFSM(Finite State Machine:有限状態機械) クラスを生成してくれます。
僕はこのSMCがいたく気に入り、更なる機能拡張を試みました。
スクリプト拡張
まず、SMCに食わすスクリプトの文法を少しばかり拡張しました。
- namespace/package指定
SMCが吐くFSMおよびFSMが参照するコンテキストをnamespaceで囲む機能を追加しました。
スクリプトのヘッダ部に、
namespace <FSM-名前空間> package <FSM-package>
と書いておけば、FSMを指定したnamespaceに置くことができます。
package指定は後述する”Javaコード生成”時の package名を指定します。
同様にコンテキストに対しては、
contextnamespace <Context-名前空間> contextpackage <Context-package>
と記述します。
- 状態が遷移しないイベント
スクリプトの状態遷移部には、
状態 { イベント 次の状態 { アクション } ... }
という形式で状態遷移を記述します。状態の遷移を伴わない場合、すなわち’次の状態’が’状態’に等しいときは、
状態 { イベント ----- { アクション } ... }
のように、ひとつ以上の’-‘を書くことができます。
- 遷移/アクションが一致するイベント
複数のイベントに対し、まったく同じ遷移/アクションを行なうとき、たとえば、
状態 { イベント-1 次の状態 { アクション } イベント-2 次の状態 { アクション } イベント-3 次の状態 { アクション } ... }
のような記述は、
状態 { イベント-1 次の状態 { アクション } イベント-2 = イベント-1 イベント-3 = イベント-1 ... }
と書くことで、イベント-2/イベント-3に対する遷移/アクションはイベント-1と同じであることを表現できます。
Javaコード生成
SMCにJavaコードを生成させたのが今回の拡張の目玉です。
fsmname GateFSM package jp.gr.java_conf.episteme.fsm namespace fsm context GateContext contextpackage jp.gr.java_conf.episteme.ct contextnamespace ct initial Locked { Locked { Coin Unlocked Unlock Pass Locked Alarm } Unlocked { Coin Unlocked ThankYou Pass Locked Lock } }
というスクリプトGateFSM.smをSMCに食わせます:
smc -j GateFSM.sm ... -j : Javaコード生成
すると、smcは以下のようなJavaソース’GateFSM.java’を生成します。
/* produced by SMC-Deluxe */ package jp.gr.java_conf.episteme.fsm; class GateFSMState { public String name() { return "(GateFSMState)"; } public void Pass(GateFSM s) { s.getContext().FSMError("Pass", s.getState().name()); } public void Coin(GateFSM s) { s.getContext().FSMError("Coin", s.getState().name()); } }; class GateFSMUnlockedState extends GateFSMState { // 省略... }; class GateFSMLockedState extends GateFSMState { // 省略... }; public class GateFSM extends jp.gr.java_conf.episteme.ct.GateContext { public GateFSM() { itsState = LockedState; } public void Pass() {itsState.Pass(this);} public void Coin() {itsState.Coin(this);} public void setState(GateFSMState theState) {itsState=theState;} public GateFSMState getState() {return itsState;}; public String getStateName() {return itsState.name();}; // 省略... }
GateFSMのベースクラスjp.gr.java_conf.episteme.ct.GateContextとテストルーチンは以下のようになります。
/* * GateContext.java */ package jp.gr.java_conf.episteme.ct; public class GateContext { public void FSMError(String event, String state) { System.out.println(state + " 状態で " + event + " が発生するはずないのに!"); } public void Lock() { System.out.println("ゲートを閉じます"); } public void ThankYou() { System.out.println("これはどうも...有り難く頂戴します"); } public void Alarm() { System.out.println("タダで入っちゃいけませんよ!"); } public void Unlock() { System.out.println("ゲートを開けます"); } } /* * gate.java */ import jp.gr.java_conf.episteme.fsm.*; public class gate { public static void main(String[] arg) throws Exception { GateFSM fsm = new GateFSM(); for ( boolean cont = true; cont; ) { System.out.println("現在の状態は " + fsm.getStateName()); System.out.print("Coin or Pass (c/p) ? "); int input = System.in.read(); System.in.skip(System.in.available()); switch ( input ) { case 'c' : fsm.Coin(); break; case 'p' : fsm.Pass(); break; default : cont = false; } } } }
FSMとコンテキストの分離
FSMはコンテキストの派生クラスというオブジェクトモデルではなく、 FSMとコンテキストを独立させるオプション’-s’を追加しました。
smc -s GateFSM.sm ... C++ smc -j -s GateFSM.sm ... Java
によって生成されたコードGateFSM.hおよびGateFSM.javaは以下のようになります。
/* * GateFSM.h */ #include "GateContext.h" namespace fsm { ... class GateFSM { ... private: GateFSMState* itsState; ct::GateContext* itsContext; public: void setContext(ct::GateContext& context) { itsContext = &context; } ct::GateContext& getContext() { return *itsContext; } ... }; } #endif /* * GateFSM.java */ package jp.gr.java_conf.episteme.fsm; ... public class GateFSM { ... public void setContext(jp.gr.java_conf.episteme.ct.GateContext context) { itsContext = context; } public jp.gr.java_conf.episteme.ct.GateContext getContext() { return itsContext; } }
このように、FSMにメソッドsetContext()が追加されます。
FSMのコンストラクトの後、setContext()によって、コンテキストを設定してください。
実行途中でsetContext()することで、コンテキストをダイナミックに入れ換えることも可能です。
コンテキスト生成
オプション’-c’で、コンテキストにひな型(C++では”コンテキスト名.h” , Javaでは “コンテキスト名.java”)を生成します。
/* * GateContext.h */ #ifndef _H_GateContext #define _H_GateContext namespace ct { class GateContext { public: void FSMError(const char*, const char*); void Lock(); void ThankYou(); void Alarm(); void Unlock(); }; } #endif /* * GateContext.java */ package jp.gr.java_conf.episteme.ct; public class GateContext { public void FSMError(String event, String state) { // code here } public void Lock() { // code here } public void ThankYou() { // code here } public void Alarm() { // code here } public void Unlock() { // code here } }
抽象コンテキスト
‘-a’オプションでコンテキストが抽象クラスとなります。
ただし、このオプションは’-s’と併用しなければなりません。
C++では全メソッドを純粋仮想関数とします。
/* * GateContext.h * (smc -c -a -s GateFSM.sm) */ #ifndef _H_GateContext #define _H_GateContext namespace ct { class GateContext { public: virtual void FSMError(const char*, const char*) =0; virtual void Lock() =0; virtual void ThankYou() =0; virtual void Alarm() =0; virtual void Unlock() =0; }; } #endif
Javaではinterfaceを定義します。
/* * GateContext.java * (smc -j -c -a -s GateFSM.sm) */ package jp.gr.java_conf.episteme.ct; public interface GateContext { void FSMError(String event, String state); void Lock(); void ThankYou(); void Alarm(); void Unlock(); }
抽象コンテキストの場合、C++/Javaそれぞれのテストルーチンは以下のようになります。
/* * gate.cpp */ #include "GateContext.h" #include "GateFSM.h" #include <iostream> using namespace std; class TheGateContext : public ct::GateContext { public: virtual void FSMError(const char*, const char*); virtual void Lock(); virtual void ThankYou(); virtual void Alarm(); virtual void Unlock(); }; void TheGateContext::Lock() { cout << "ゲートを閉じます" << endl; } void TheGateContext::Unlock() { cout << "ゲートを開けます" << endl; } void TheGateContext::Alarm() { cout << "タダで入っちゃいけませんよ!" << endl; } void TheGateContext::ThankYou() { cout << "これはどうも...有り難く頂戴します" << endl; } void TheGateContext::FSMError(const char* event, const char* state) { cout << state << " 状態で " << event << " が発生するはずないのに!" << endl; } int main() { TheGateContext context; fsm::GateFSM fsm; fsm.setContext(context); for ( bool cont = true; cont; ) { char input[32]; cout << "現在の状態は : " << fsm.getState().name() << endl; cout << "Coin or Pass (c/p) ? " << flush; cin >> input; switch ( *input ) { case 'c' : fsm.Coin(); break; case 'p' : fsm.Pass(); break; default : cont = false; } } return 0; } /* * gate.java */ import jp.gr.java_conf.episteme.fsm.*; import jp.gr.java_conf.episteme.ct.*; class TheGateContext implements GateContext { public void FSMError(String event, String state) { System.out.println(state + " 状態で " + event + " が発生するはずないのに!"); } public void Lock() { System.out.println("ゲートを閉じます"); } public void ThankYou() { System.out.println("これはどうも...有り難く頂戴します"); } public void Alarm() { System.out.println("タダで入っちゃいけませんよ!"); } public void Unlock() { System.out.println("ゲートを開けます"); } } public class gate { public static void main(String[] arg) throws Exception { GateFSM fsm = new GateFSM(); GateContext context = new TheGateContext(); fsm.setContext(context); for ( boolean cont = true; cont; ) { System.out.println("現在の状態は " + fsm.getStateName()); System.out.print("Coin or Pass (c/p) ? "); int input = System.in.read(); System.in.skip(System.in.available()); switch ( input ) { case 'c' : fsm.Coin(); break; case 'p' : fsm.Pass(); break; default : cont = false; } } } }