エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

有限オートマトン Finite Automaton はPrologで書くと簡単だった件

引き続き、アンダースタンディング コンピュテーションを読んでいます。


先日はいろんな言語でDFAを書いてみたわけですが、よくよくコードを読み直してみると、Prologのばあいはもっと簡単にDFAを書けることに気がつきました。
というか、Prologのコードは、ほとんどルールそのものだった、というお話。

決定性有限オートマトン: Deterministic Finite Automaton (DFA)

DFAでは、ある状態である入力があったときの遷移先が1つに決定しています。
次のような状態遷移表で表されるDFAを書いてみます。

状態\入力 a b
1 2 1
2 2 3
3 3 3


DFAのコード。

rule(1, 0'a, 2). rule(1, 0'b, 1).
rule(2, 0'a, 2). rule(2, 0'b, 3).
rule(3, 0'a, 3). rule(3, 0'b, 3).

dfa(State, [], State).
dfa(StartState, [C|CS], LastState) :-
  rule(StartState, C, NextState),
  dfa(NextState, CS, LastState).

isAccepts(StartState, AcceptStates, String, true) :-
  dfa(StartState, String, LastState),
  member(LastState, AcceptStates).
isAccepts(_, _, _, false).


DFAをテストするコード。開始状態は 1 、受理状態は 3 です。

test(StartState, AcceptStates, String) :-
  isAccepts(StartState, AcceptStates, String, Result),
  format("~s => ~p~n", [String, Result]).

main :-
  test(1, [3], "a"),
  test(1, [3], "baa"),
  test(1, [3], "baba").


実行結果。

$ swipl -s dfa.prolog -g 'main, halt'
a => false
baa => false
baba => true

非決定性有限オートマトン: Nondeterministic Finite Automaton (NFA)

NFAでは遷移先が1つに決まりません。そのため、各々の遷移先に対する挙動を考慮しなければなりません。

状態\入力 a b
1 1 1 or 2
2 3 3
3 4 4

…なのですが。Prologにはバックトラックというしくみが言語自体に組み込まれています。これによって、複数の遷移先があるばあいでも各々の挙動を網羅することができるようになっています。


ja.wikipedia.org に解説がありますので、詳しくはこちらへ:


NFAのコード。
実際のところ、rule の内容を変更し、名前を dfa から nfa に変えただけで、処理自体は DFA と同じです。

rule(1, 0'a, 1). rule(1, 0'b, 1). rule(1, 0'b, 2).
rule(2, 0'a, 3). rule(2, 0'b, 3).
rule(3, 0'a, 4). rule(3, 0'b, 4).

nfa(State, [], State).
nfa(StartState, [C|CS], LastState) :-
  rule(StartState, C, NextState),
  nfa(NextState, CS, LastState).

isAccepts(StartState, AcceptStates, String, true) :-
  nfa(StartState, String, LastState),
  member(LastState, AcceptStates).
isAccepts(_, _, _, false).


ちょっと挙動を確認してみます。

$ swipl -s nfa.prolog
?- nfa(1, "b", R).
R = 1 ;
R = 2 ;
false.

swipl -s nfa.prolog でファイルを読み込んでいます。
プロンプトが表示されるので nfa(1, "b", R). と入力します。
実行結果として R = 1 と表示され、一旦入力待ちになります。ここでセミコロンを入力すると別の解を求めるため実行を再開します。
別の結果として R = 2 と表示されます。これらはそれぞれ、状態1のときに入力Bがあったばあいの結果(遷移先の状態)の状態1と状態2に対応します。
2つ目の解を表示したあと再び入力待ちになっているのでセミコロンを入力すると、これ以上解がないので false. を表示して処理を終了します。


NFAをテストするコード。開始状態は 1 、受理状態は 4 です。

test(StartState, AcceptStates, String) :-
  isAccepts(StartState, AcceptStates, String, Result),
  format("~s => ~p~n", [String, Result]).

main :-
  test(1, [4], "bab"),
  test(1, [4], "bbbbb"),
  test(1, [4], "bbabb").


実行結果。

$ swipl -s nfa.prolog -g 'main, halt'
bab => true
bbbbb => true
bbabb => false

自由移動のある NFA

入力がなくても自発的に遷移する自由移動があるばあいを考えます。
下記の状態遷移表ではεで書いた列が自由移動です。遷移先が - となっている部分は、そのような遷移のルールがないことを表しています。

状態\入力 a ε
1 - 2 or 4
2 3 -
3 2 -
4 5 -
5 6 -
6 4 -

状態1は自由移動で、つまり入力がなくても自発的に、状態2か状態4に遷移します。


自由移動のあるNFAのコード。
入力を必要としない rule/2 と、 rule/2 を利用して入力をとらずに遷移するコードを追加しています。

rule(1, 2).
rule(1, 4).
rule(2, 0'a, 3).
rule(3, 0'a, 2).
rule(4, 0'a, 5).
rule(5, 0'a, 6).
rule(6, 0'a, 4).

nfa(State, [], State).
nfa(StartState, CS, LastState) :-
  rule(StartState, NextState),
  nfa(NextState, CS, LastState).
nfa(StartState, [C|CS], LastState) :-
  rule(StartState, C, NextState),
  nfa(NextState, CS, LastState).

isAccepts(StartState, AcceptStates, String, true) :-
  nfa(StartState, String, LastState),
  member(LastState, AcceptStates).
isAccepts(_, _, _, false).


ちょっと挙動を確認してみます。

?- nfa(1, "", R).
R = 1 ;
R = 2 ;
R = 4 ;
false.

状態1のばあい、入力がなくても状態2か状態4に遷移することがわかります。遷移せず状態1のままという場合もあるので、このばあいは状態1,2,4のいずれかが取りうる値となります。


自由移動のあるNFAをテストするコード。開始状態は 1 、受理状態は 2 か 4 です。

test(StartState, AcceptStates, String) :-
  isAccepts(StartState, AcceptStates, String, Result),
  format("~s => ~p~n", [String, Result]).

main :-
  test(1, [2, 4], "aa"),
  test(1, [2, 4], "aaa"),
  test(1, [2, 4], "aaaa"),
  test(1, [2, 4], "aaaaa").


実行結果。

$ swipl -s nfa.prolog -g 'main, halt'
aa => true
aaa => true
aaaa => true
aaaaa => false

いつか読むはずっと読まない:人工知能つながりということで…

久々に小説を読んでいます。これがなかなか進みません。小説を読む体力が低下しているようです。

月は無慈悲な夜の女王 (ハヤカワ文庫 SF 1748)

月は無慈悲な夜の女王 (ハヤカワ文庫 SF 1748)