2011/03/27

Objective-Cで有限オートマトンを作ろう

【ミッション: お買い物プログラムを作れ】

今日は、Objective-C のプロトコルを駆使して有限オートマトン(finite automaton)、別名:有限状態機械(finite state machine)を作りつつ、オブジェクト指向プログラミングに慣れていこうと思う。

とは言ってもそれほど難しいものではなく、今回作るのは、ユーザの入力によって以下のように状態が遷移する、簡単な『お買い物プログラム』だ。
※ 突っ込みどころが満載なのはデフォなので無視してほしい。

最初に断っておくと、状態遷移を管理するプログラムの実装自体は、特別なテクニックが無くとも(一応)可能である。



上記の状態遷移を直線的に列挙すると、おおむね以下の通り。
1. 『いらっしゃいませ』 A. 買いにきた(→2) B. 売りにきた(→9)

2. 『お買い上げですね』 A. 武器(→3) B. 防具(→6)

3. 『どれにいたしますか?』 A. 剣(→4) B. 銃(→5)

4. 『剣でよろしいですね?』 A. はい(→10) B. いいえ(→3)

5. 『銃でよろしいですね?』 A. はい(→10) B. いいえ(→3)

6. 『どれにいたしますか?』 A. 鎧(→7) B. 兜(→8)

7. 『鎧でよろしいですね?』 A. はい(→10) B. いいえ(→6)

8. 『兜でよろしいですね?』 A. はい(→10) B. いいえ(→6)

9. 『本当に売りますか?』 A. はい(→10) B. いいえ(→10)

10. 『ほかに御用はありますか?』 A. はい(→1) B. いいえ(→11)

11. 『ありがとうございました』 (→End)

この処理を素直に実装しようとすると、11種類の状態とその遷移先をすべて if 文で振り分ける方法がまず思いつく。

敢えてこの方法のソースコードは示さないが(ホント面倒なので)、気になる方には是非実装して頂きたい。間違いなく途中で厭になる



上記方針で実装されたプログラムは、ソースコードの可読性だけでなく、拡張性までをも失う事になる。新たに状態を追加したくなったときには、おびただしい if 文に手を加える必要があるからである。もちろん、状態が正しく遷移する事を確認するのも大変だ。

しかし、1つ1つの状態が共通の振る舞いを持っている事に気付けば、オブジェクト指向の考え方を用いてよりスマートに実装できる。

簡単のため、こんなオートマトンを考えてみよう。

まず、共通の振る舞いをまとめた抽象クラス State を作る。各状態の共通の振る舞いは《次に遷移する状態を決定する》という更新処理であり、ここでは update メソッドと名づけておく。

  1. // ===============================================  
  2. // 抽象クラス(…に相当するもの。継承して使う)  
  3. // ===============================================  
  4. @interface State : NSObject  
  5. - (State *) update;  
  6. @end  
  7.   
  8. @implementation State  
  9. - (State *) update {  
  10.     return (State *)Nil;  
  11. }  
  12. @end  

Objective-C には、抽象クラスを作るための特別な構文がないので、傍目には普通のクラスと見分けがつかない。そのため、不慮のバグを見つけやすくするために State クラスは常に Nil を返すようにしている。

「どういうときに・どこへ遷移するか」は、各状態によって異なるため、State を継承した状態クラス State1State2State3 を定義する。update メソッドをオーバーライドし、個別に更新処理を記述すればよい。

戻り値が State * 型なのは、update メソッドが次に遷移する状態のインスタンス(のポインタ)を返すようにするためである。

  1. // ===============================================  
  2. // 各状態クラス ~インタフェース部~  
  3. // ===============================================  
  4. @interface State1 : State  
  5. - (State *) update;  
  6. @end  
  7.   
  8. @interface State2 : State  
  9. - (State *) update;  
  10. @end  
  11.   
  12. @interface State3 : State  
  13. - (State *) update;  
  14. @end  
  15.   
  16. // ===============================================  
  17. // 各状態クラス ~実装部~  
  18. // ===============================================  
  19. @implementation State1  
  20. - (State *) update {  
  21.     printf("ここは状態1です∩( ・ω・)∩\n");  
  22.     getchar();  
  23.     return [[State2 alloc] init];  
  24. }  
  25. @end  
  26.   
  27. @implementation State2  
  28. - (State *) update {  
  29.     printf("ここは状態2だよヾ(・ω・)ノ゛\n");  
  30.     getchar();  
  31.     return [[State3 alloc] init];  
  32. }  
  33. @end  
  34.   
  35. @implementation State3  
  36. - (State *) update {  
  37.     printf("ここは状態3である(´-ω-`)\n");  
  38.     getchar();  
  39.     return [[State2 alloc] init];  
  40. }  
  41. @end  

これで準備は完了だ。各状態クラスをこのように作っておくだけで、main 関数はこんなにシンプルに書ける。

  1. main() {  
  2.     State *state = [[State1 alloc] init];  
  3.     State *next;  
  4.       
  5.     while(next = [state update]) {  
  6.         if(state != next) {  
  7.             [state release];  
  8.             state = next;  
  9.             printf("遷移しました!\n");  
  10.         }  
  11.     }  
  12. }  

ちなみに実行すると、こうなる(何か入力があるたびに、問答無用で次の状態へと遷移する)。


状態遷移図の通り、初期状態 1 から、以降、状態 2 と 3 を交互に遷移している事が判る。



【プログラムのからくり】

State から派生した State1State2State3 のインスタンスは、いずれもアップキャストする事で State と同一視して扱う事が可能になる。

この性質を利用して、main の中でポインタの継ぎ換えを行い、状態遷移を実現するのだ。ここだけ詳しく見ていこう。

はじめに、State *state = [[State1 alloc] init]; で、変数 state には状態 1 のポインタが格納される。


この状態で [state update] を実行すると、状態 1 のインスタンスが次状態(状態 2 )のインスタンスを生成し、戻り値として返す。


これを、next に一時的に格納する。

実は、Java ではこの一時変数は必要なく、いきなり state に次状態を格納しても大した問題は起きないのだが、Objective-C で同じようなマネをしたら大問題になる。そう、メモリリークだ。

そこで、statenext を比較して、異なる先を参照している場合は、ちゃんと release をしてからポインタを継ぎ換える必要がある。


これによって state の中身はどんどん更新され、update を実行すると、(現在の状態に応じて)振る舞いが変わる。この多態性と呼ばれる性質のお陰で、現在の状態が何であるかをプログラマが意識する事はほとんどなく、スマートにプログラムを記述できるのだ。

これが、オブジェクト指向による状態遷移実装のからくりである。



ちなみに、抽象クラスではなく、プロトコルを用いて上記のプログラムを書き換える事ができる。

  1. #import <Foundation/Foundation.h>  
  2.   
  3. // ===============================================  
  4. // プロトコル  
  5. // ===============================================  
  6. @protocol State  
  7. // 更新メソッド  
  8. // 次の状態(へのポインタ)を返す  
  9. - (id) update;  
  10. @end  
  11.   
  12.   
  13. // ===============================================  
  14. // 各状態クラス ~インタフェース部~  
  15. // ===============================================  
  16. @interface State1 : NSObject<State>  
  17. - (id) update;  
  18. @end  
  19.   
  20. @interface State2 : NSObject<State>  
  21. - (id) update;  
  22. @end  
  23.   
  24. @interface State3 : NSObject<State>  
  25. - (id) update;  
  26. @end  
  27.   
  28. // ===============================================  
  29. // 各状態クラス ~実装部~  
  30. // ===============================================  
  31. @implementation State1  
  32. - (id) update {  
  33.     printf("ここは状態1です∩( ・ω・)∩\n");  
  34.     getchar();  
  35.     return [[State2 alloc] init];  
  36. }  
  37. @end  
  38.   
  39. @implementation State2  
  40. - (id) update {  
  41.     printf("ここは状態2だよヾ(・ω・)ノ゛\n");  
  42.     getchar();  
  43.     return [[State3 alloc] init];  
  44. }  
  45. @end  
  46.   
  47. @implementation State3  
  48. - (id) update {  
  49.     printf("ここは状態3である(´-ω-`)\n");  
  50.     getchar();  
  51.     return [[State2 alloc] init];  
  52. }  
  53. @end  
  54.   
  55. main() {  
  56.     id state = [[State1 alloc] init];  
  57.     id next;  
  58.       
  59.     while(next = [state update]) {  
  60.         if(state != next) {  
  61.             [state release];  
  62.             state = next;  
  63.             printf("遷移しました!\n");  
  64.         }  
  65.     }  
  66. }  

なお、抽象クラスのときと違って、update の戻り値を State * (プロトコルのつもり)にしてしまうと、なぜか Sytax Error になってしまう。恐らく、プロトコルは型名として扱われない仕様なのだろう。戻り値の型を id 型に変更したらエラーが消えた。

これはインタフェースにアップキャストできる Java や C++ では考えられない現象だったので、ちょっとだけ嵌ったポイントでもある。



ここまで書けば、あとは『お買い物プログラム』の作成は簡単である。

一応すごく適当に作ったソースを載せておくが、もうちょっと各状態クラス名をマシなものにすれば、一瞥しただけで遷移先が把握できるようになると思う。


実行すると、こんな感じでちゃんと遷移できている事が判る。やったね!地味だけど。


ちなみに今日のお話は、GoF のデザインパターンのうち、『State パターン』と呼ばれているものである。

ゲームの画面遷移などで有効なテクニックだ(とぼくは信じている)。

あと、『遷移するたびにメモリを alloc するのってどうよ?』と思われるかも知れない。alloc は非常にオーバヘッドの大きな処理なので、それなりに考慮すべき事なのだが、これを考え始めると頭が痛くなるので今日はここまで。