このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

正規表現とオートマトン:なんだ簡単じゃん、JavaScriptによる実装

先週のエントリー「この機会にマスターしようぜ、正規表現、構文図、オートマトン」において、正規表現オートマトンの理屈は説明しました。んじゃ、適当な正規表現に対応するオートマトンを作ってみましょう。

非決定性オートマトンはめんどくさいので決定性オートマトンにします。正規表現から作ったオートマトンが非決定性になってしまったときは、がんばって決定性オートマトンに変形するかあきらめるかしてください。

例題となる正規表現オートマトンは、「Erlang実験室:状態遷移を書くのはこんなに簡単」で使ったものをそのまま流用します。

図と表のなかで、EOSは End Of String のマーカー、◎は終状態です

遷移表: 0から3までの各状態について、入力ごとの遷移先は次の通り。×はエラーです。

状態 文字a 文字b 文字c EOS その他
0 1 × × × ×
1 2 2 3 × ×
2 × 2 3 × ×
3 × × × ×

今回はJavaScriptで実装します。EOSにはドル記号'$'を使い、終状態は状態番号を-1として識別します。状態遷移表を、switch文のようなプログラムの制御構造として表現するのではなくて、データとして与えます。状態遷移表のデータを入れ替えれば、色々なオートマトンを実現できます。

上の表に対応する状態遷移を表すデータは次のとおり。


var transition = [
/* 0 */ {"a" : 1},
/* 1 */ {"a" : 2, "b" : 2, "c" : 3},
/* 2 */ {"b" : 2, "c" : 3},
/* 3 */ {"$" : -1}
];

オブジェクトを項目とする配列データですね。配列の位置が現状態、オブジェクトのプロパティ名が入力、オブジェクトのプロパティ値が次の状態を表しています。

オートマトンはAutomatonというクラスにしているので、コンストラクタに状態遷移表データを渡してオートマトンインスタンスを生成します。オートマトンの実行は run というメソッドを使うことにします。次はFirebugでの実行例。ソースはその後に貼りつけてあります。さらにその後に注意事項が書いてあります。


>>> var autom = new Automaton(transition);
>>> autom.run("abc");
[Result] abc is accepted.
>>> autom.debug = true;
true
>>> autom.run("abc");
[Trace] state:0 input:a next:1
[Trace] state:1 input:b next:2
[Trace] state:2 input:c next:3
[Trace] state:3 input:$ next:-1
[Result] abc is accepted.
>>> autom.run("aabc");
[Trace] state:0 input:a next:1
[Trace] state:1 input:a next:2
[Trace] state:2 input:b next:2
[Trace] state:2 input:c next:3
[Trace] state:3 input:$ next:-1
[Result] aabc is accepted.
>>> autom.run("aaabc");
[Trace] state:0 input:a next:1
[Trace] state:1 input:a next:2
[Trace] state:2 input:a next:undefined
[Result] aaabc is refuted.
>>> autom.run("axbc");
[Trace] state:0 input:a next:1
[Trace] state:1 input:x next:undefined
[Result] axbc is refuted.
>>> autom.run("abbc");
[Trace] state:0 input:a next:1
[Trace] state:1 input:b next:2
[Trace] state:2 input:b next:2
[Trace] state:2 input:c next:3
[Trace] state:3 input:$ next:-1
[Result] abbc is accepted.

Automaton.js :

/* Automaton.js */

function Automaton(transition) {
    this.transition = transition;
    this.state = 0; // 0 は始状態
    this.debug = false;
    this._display = (console && console.firebug && console.log)? console.log : alert;
}

Automaton.ACCEPT = 1;
Automaton.REFUTE = 0;
Automaton.MORE =  2;

Automaton.prototype.accept = function(input) {
    var next = this.transition[this.state][input];
    this.trace(input, next);
    if (next === -1) {
        return Automaton.ACCEPT; // 受理
    }
    if (next === undefined) {
        return Automaton.REFUTE; // 拒否
    }
    this.state = next;
    return Automaton.MORE; // 状態遷移して継続
};

Automaton.prototype.trace = function(input, next) {
    if (this.debug) {
        this._display("[Trace] state:" + this.state + " input:" + input + " next:" + next);
    }
};

Automaton.prototype.reset = function() {
    this.state = 0;
};

Automaton.prototype.run = function(inputString) {
    var s = inputString;
    if (s.charAt(s.length - 1) != "$") {
        s = s + "$";
    }
    this.reset();
    for (var i = 0; i < s.length; i++) {
        var result = this.accept(s.charAt(i));
        switch (result) {
        case Automaton.ACCEPT :
            this.sayResult(inputString + " is accepted.");
            return;
        case Automaton.REFUTE :
            this.sayResult(inputString + " is refuted.");
            return;
        case Automaton.MORE :
            ; // continue loop
        }
    }
    this.sayResult("Something wrong.");
};

Automaton.prototype.sayResult = function(str) {
    this._display("[Result] " + str);
};

autom.html :

<html><!-- autom.html -->
  <head>
    <title>Automaton Test</title>
    <script src="Automaton.js"></script>
    <script>
      var transition = [
       /* 0 */ {"a" : 1},
       /* 1 */ {"a" : 2, "b" : 2, "c" : 3},
       /* 2 */ {"b" : 2, "c" : 3},
       /* 3 */ {"$" : -1}
      ];
    </script>
  </head>
  <body>
    <h1>Automaton Test</h1>
  </body>
</html>

注意事項

入力の終りはドル記号で識別しますが、文字列の最後にドル記号がなくてもメソッドrunの内部で付け足します。よって、runに渡す文字列にドル記号がなくても大丈夫です。文字列の中間にドル記号を入れると、そこで入力が終ります。


>>> autom.run("abc$aa");
[Trace] state:0 input:a next:1
[Trace] state:1 input:b next:2
[Trace] state:2 input:c next:3
[Trace] state:3 input:$ next:-1
[Result] abc$aa is accepted.

「この機会にマスターしようぜ、正規表現、構文図、オートマトン」では、「終状態にいるときにエンドマーカー記号(この場合はドル記号)を入力すると受理が成功」でしたが、今回の実装では、「エンドマーカー記号を入力しての遷移先が終状態なら受理が成功」です。これは、「Erlang実験室:状態遷移を書くのはこんなに簡単」の例をそのまま使ったため*1です。

微妙な違いはありますが、本質的な差ではありません。自分の好みや実装上の都合で適当に決めてもかまいません。次のコードは、「状態番号が最大の状態が終状態」という約束のもとで、「終状態にいるときにエンドマーカー記号(この場合はドル記号)を入力すると受理が成功」*2という判断を書き下したものです。これを使うなら、状態遷移データの側にはエンドマーカー記号に対する遷移を書きません。

Automaton.EndOfInput = '$';
Automaton.finalState = function (trans) {
    return trans.length - 1;
};

Automaton.prototype.accept = function(input) {
    var next = this.transition[this.state][input];
    this.trace(input, next);
    if (input == Automaton.EndOfInput) {
        if (this.state === Automaton.finalState(this.transition)) {
            return Automaton.ACCEPT; // 受理
        } else {
            return Automaton.REFUTE; // 拒否
        }
    }
    if (next === undefined) {
        return Automaton.REFUTE; // 拒否
    }
    this.state = next;
    return Automaton.MORE; // 状態遷移して継続
};

*1:なぜ流用したかというと、新しく図や表を書くのが面倒だったから ^^;

*2:同じことですが、「エンドマーカー記号が入力されたときに終状態にいるなら受理成功」のほうがコードに近い日本語ですね。