【社内勉強会】PHPでInterPreterパターン入門


そろそろ半分消化です。

先週は東京出張と日程が重なってしまったため、第十二回目のEmperorさんによるBuilderパターン、第十三回目の同じくEmperorさんによるDecoratorパターンは参加できませんでした。
いつの日か、補講を開いてください。お願いします。

で、第十四回目、担当は自分。
またも鬼門、Interpreterパターンに挑戦です。

wikipedia曰く、

Interpreter パターンは、コンピュータプログラミングにおけるデザインパターンの一つである。Interpreter パターンの基本的な考えは、定義された種類の問題を素早く解くために、ドメインに特化した言語を実装することである。特化言語は汎用の言語よりも数倍から数百倍高速に問題を解ける場合が多い。

よくわからないので、GoF本曰く、

言語に対して、文法表現と、それを使用して文を解釈するインタプリタを一緒に定義する。

これでもよくわかりませんが、個人的な解釈だと、

PHP(とか)でオレオレ言語をつくっちゃおう

って感じです。

結城本の言葉を借りると、

  • オレオレ言語→「ミニ言語」
  • 「ミニ言語」で書かれたプログラム→「ミニ・プログラム」

で、この「ミニ言語」で書かれた「ミニ・プログラム」が動くように、PHPなりなんなりで「ミニ言語」を解釈するインタープリタを実装する場合に使われるのがInterpreterパターンです。
「ミニ言語」が動作できるようにするための「通訳(interpreter)」の役目を果たすわけですね。

ミニ言語ってどんなの?

GoF本が言うところの、「文法表現」。
自分的解釈だと「オレオレ言語」。

命令やら、文法やらをマイルールで決めちゃいます。
繰り返し使うような処理をまとめちゃえばシンプルに記述することができるようになるとか。

もーちょっと難しい言葉を引用すると、

  • 記述されたコードの文字を意味のある字句(トークン)に分解する(字句解析)
  • 字句解析の結果を基に文法に従っているかどうかチェックする(構文解析)

この二つを行うことができる実装を用意して、目的の処理を行う手法のことをミニ言語と言うんじゃないかなー。

で、本やらサイトやらで紹介されている感じのミニ言語。

<Job> ::= start <CommandList>
<CommandList> ::= <Command>* end
<Command> ::= df | date | pwd

こんなやつですね。

順番に、

  • このミニ言語は「begin」という文字列から始まる
  • そのあとに、コマンドが0個以上あって、最後は「end」という文字列で終わる
  • コマンドの種類は「df」、「date」、「pwd」がある

という意味になります。
なんとなく雰囲気は掴めますね。
この図の左辺の<>がクラスになります。

ちなみに、この書き方のことをBNF(Backus Naur Form)といいます。

BNFってなに?

ミニ言語文法を記述する際に使用される記法のことです。
コンピュータ言語の文法を定義する際に使われるようです。
Pythonのリファレンスなんかにも使われていました。

詳細はwikipediaからどうぞ。

クラス図は?

Compositeパターンに似ていますね。

  • AbstractExpressionインターフェイスは実装するクラスにAPIとなるInterpret()強制します。
  • TerminalExpressionクラスは構文木でいう、葉の部分になります。子要素をもたないクラスです。
  • NonterminalExpressionクラスは構文木でいう、枝の部分になります。今回の例では省略しましたが、再帰的な実装も可能です。
  • Contextクラスは構文解析に必要な情報やメソッドを持つクラスです。
  • Clientクラスはクライアント。

実装は?

<?php

interface Command {
    public function execute(Context $context);
}

// NonterminalExpressionクラス
class JobCommand implements Command {
    public function execute(Context $context) {
        if ($context->getCurrentCommand() !== 'start') {
            throw new RuntimeException('illegal command ' . $context->getCurrentCommand());
        }
        $command_list = new CommandListCommand();
        $command_list->execute($context->next());
    }
}

// NonterminalExpressionクラス
class CommandListCommand implements Command {
    public function execute(Context $context) {
        while (true) {
            $current_command = $context->getCurrentCommand();
            if (is_null($current_command)) {
                throw new RuntimeException('"end" not found ');
            } elseif ($current_command === 'end') {
                break;
            } else {
                $command = new CommandCommand();
                $command->execute($context);
            }
            $context->next();
        }
    }
}

// TerminalExpressionクラス
class CommandCommand implements Command {
    public function execute(Context $context) {
        $current_command = $context->getCurrentCommand();
        if ($current_command === 'df') {
            $df = `df`;
            echo $df . "\n";
        } elseif ($current_command === 'date') {
            $date = `date`;
            echo $date . "\n";
        } elseif ($current_command === 'pwd') {
            $pwd = `pwd`;
            echo $pwd . "\n";
        } else {
            throw new RuntimeException('invalid command [ ' . $current_command . ']');
        }
    }
}

// Contextクラス
class Context {
    private $commands;
    private $current_index = 0;
    private $max_index = 0;

    public function __construct($command) {
        $this->commands = split(' +', trim($command));
        $this->max_index = count($this->commands);
    }

    public function next() {
        $this->current_index++;
        return $this;
    }

    public function getCurrentCommand() {
        if (!array_key_exists($this->current_index, $this->commands)) {
            return null;
        }
        return trim($this->commands[$this->current_index]);
    }
}
function execute($command) {
    $job = new JobCommand();
    try {
        $job->execute(new Context($command));
    } catch (Exception $e) {
        echo htmlspecialchars($e->getMessage(), ENT_QUOTES);
    }
    echo '<hr>';
}

$command = 'start df date pwd end';
execute($command);
/*
Filesystem           1K-ブロック    使用   使用可 使用% マウント位置
/dev/mapper/VolGroup00-LogVol00
                     480195320   3538080 452264744   1% /
/dev/sda1               197430     27108    160129  15% /boot
tmpfs                  1032764         0   1032764   0% /dev/shm

2010年  7月 27日 火曜日 16:46:04 JST

/home/paz/public_html
*/

こんな感じ。
サンプルはほぼクッキー本です。

どんな時に使うの?

wikipedia曰く、

データベースに特化した問い合わせ言語(SQLなど)
通信プロトコルを記述するために良く用いられる特化したコンピュータ言語
特化言語を導入した汎用のコンピュータ言語

結城本、クッキー本曰く、

バッチ処理のような、特定の処理を繰り替えしたり、順番に実行したりすることで大きな処理を行うとき

バッチ処理用にPHPでミニ言語を作ることはできそうですが、Linuxのコマンド使えよ・・・って思いが強いです。

これに関しては勉強会までに発見があったら追記します。

その他

正規表現はこの類のものらしい

hoge ( fuga | moge ) *

これでhogeの後に、fugaかmogeの0回以上の繰り返し・・・みたいな
でも、実際の正規表現はもっと複雑なことをしているらしい。

他にも、sprintf()関数や、sendmailの設定ファイルなんかも・・・

ここまで、お疲れ様でした。

, , ,

  1. No comments yet.
(will not be published)
  1. No trackbacks yet.