株式会社エス・スリー・フォー

SMC(State Map Compiler) の拡張

はじめに

C Magazine 1999/9 で、SMC(State Map Compiler)を紹介しました。 SMCに状態遷移表を記述したスクリプト(テキストファイル)を食わせると、状態遷移表に書かれた通りに動作するFSM(Finite State Machine:有限状態機械) クラスを生成してくれます。

僕はこのSMCがいたく気に入り、更なる機能拡張を試みました。src

スクリプト拡張

まず、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;
      }
    }
  }

}