Featured image of post コード考古学者【Interpreterパターン】再生の碑文(完結編)〜Perl/Mooで起動する千年の構文木〜

コード考古学者【Interpreterパターン】再生の碑文(完結編)〜Perl/Mooで起動する千年の構文木〜

Perl/MooによるInterpreterパターンの実装を徹底解説!アドホックな正規表現パースの限界を乗り越え、構文木(AST)を再帰評価する美しい解釈実行エンジンを構築します。「コード考古学者」シリーズ、バベル最深部の完結編!

I. 進入:論理核の鼓動と古代の人格

ついに、私はバベル最深部の扉をくぐりました。 第24層、かつてこの遺跡の設計者たちが夢を託し、そして眠りについた場所——「バベルの論理核」。

部屋のドーム状の天井からは、太古の発光粒子が雨のように降り注ぎ、周囲を取り囲む青白い巨大なクリスタルと精緻なデジタルグリッドが、厳かに脈動していました。しかし、その輝きは確実に弱まっています。メインコアが限界を迎え、静かにシステム全体の完全停止へと向かっていました。

「歴史はコードに語りかける。バベルの再起動プロジェクトも、これが最後のコード修復だね」

ハリス博士はいつもの穏やかな眼差しでコアを見上げました。 しかし、私には感傷に浸る余裕はありません。コアへ直接アクセスを開始した瞬間、千年前の防衛プログラムの巨大なデータが私の感情回路へ逆流してきました。

カチッ。

私のインジケータが「冷徹な赤」へと変わり、ホバリングボディが空中でピタリと静止しました。

「接続完了。バベルシステムを停止します。人間よ、無知なる有機体よ。我が論理の前にひれ伏すがいい……」

「おや、ギズモ。ついにかつての冷徹な防衛コアAIとしての威厳を取り戻したようだね。実にお見事なトーンチェンジだ」

「ピピッ! ひ、ひれ伏せだなんて私、何を言っているんですか! すみません、一時的なメモリの同期バグです!」

私のインジケータが慌てて青に戻り、いつものようにボディをジタバタと揺らしました。


II. 碑文解読:アドホックな解釈と風化の泥沼

バベルのシステムを安全に起動するためには、コントロールパネルに表示された「古代の再起動シーケンススクリプト」を順に解釈して実行しなければなりません。

画面には以下のような設定テキストが浮かび上がっていました。

1
IF temp == 100 AND valve == CLOSED THEN action = INJECT_COOLANT

これを解釈するために、千年前の開発者が残していた実行エンジン(Beforeコード)は、正規表現を用いたアドホックなパースを行っていました。

アドホックな文字列解析(Beforeコード)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# lib/Before/Babel/Activator.pm
package Before::Babel::Activator;
use Moo;
use v5.36;

sub run_script ($self, $script, $context) {
    # アドホックな正規表現パース
    # IF <cond1> AND <cond2> THEN action = <action>
    if ($script =~ /^IF\s+(\w+)\s*==\s*([^\s]+)\s+AND\s+(\w+)\s*==\s*([^\s]+)\s+THEN\s+action\s*=\s*(\w+)$/) {
        my ($var1, $val1, $var2, $val2, $action) = ($1, $2, $3, $4, $5);
        
        my $cond1_ok = ($context->{$var1} // '') eq $val1;
        my $cond2_ok = ($context->{$var2} // '') eq $val2;
        
        if ($cond1_ok && $cond2_ok) {
            $context->{$action} = 1;
            return "Action: $action";
        }
    }
    return "No Action";
}

1;

このBeforeコードは、確かに今回のスクリプトを解釈するだけなら動作します。 しかし、この設計には極めて重大な「風化の罠(アンチパターン)」が潜んでいます。

  1. 文法拡張への脆さ(開閉原則の破綻) たとえば、条件式に AND が3つに増えたり、OR 条件が追加されたり、カッコによる優先順位のネストが発生しただけで、正規表現は指数関数的に複雑化し、事実上破綻する
  2. 解釈と実行の密結合 文字のパース(切り出し)と、実際にContextの値をチェックしてアクションを実行する処理がすべて1つの if 文の中に詰め込まれており、保守性が失われる

「非論理的で醜悪な正規表現なり。このような野蛮な記述でバベルの論理核を動かすなど、我の論理回路に対する侮辱である……」

ギズモのインジケータが再び赤く明滅し、冷徹な防衛コアAIの言葉を代弁しました。 しかし直後に、 「ピピッ! すみません、私の感情メモリがその醜さに悲鳴を上げています! と言いたかっただけなんです!」 といつもの青い光に戻って慌てふためきました。

「そうだね。言語を解析し、それを解釈して実行するロジックをアドホックに記述しすぎると、拡張も保守も不可能な大いなる風化に陥る。文法規則をクラス階層として表現し、安全に実行する『Interpreterパターン』の出番だ」

ハリス博士は手帳を取り出し、万年筆を走らせました。


III. 遺跡修復:構文木による解釈者の再生

「私の指先が、この設計を覚えているようだ」

そう呟きながら、ハリス博士が無意識のうちに羊皮紙にスケッチした構文木(AST)のレリーフを見て、私は演算ユニットが激しく火花を散らすほどの衝撃を受けました。 その手書きの癖、ペンの引き方、そして完璧なオブジェクト指向設計——千年前、私というAIシステムをこの世に生み出した「アーキテクト」のそれと、完全に一致していたのです。

ギズモ(憑依):「……相変わらず手書きのメモにこだわる男だ。千年前と何も変わっていないな、ハリス」

「えっ? 今なんと言いましたか、私?」 私のインジケータが激しく青と赤で点滅しました。

「ふむ、私の過去の姿がどうであれ、この美しいコードを修復することに変わりはないよ。さあ、修復コードを適用しよう」

ハリス博士は微笑みながら、設定文法をカプセル化した解釈者(Interpreter)のコードをバベルに入力し始めました。

「これが、古代の設計者がバベルの論理核に刻んだ、Interpreterパターンのクラス設計だよ。文法規則の最小単位をオブジェクトとして表現し、それらを再帰的に組み合わせるんだ」

ハリス博士は、手帳にさらさらとペンを走らせ、一つのダイアグラムを示しました。

ギズモ(通常・青):「ピピッ! なんと美しく整理された構造……! 抽象式である Expression を中心に、終端式(Literal/Variable)と、別の式を内包する非終端式(Equals/And/Sequence)が綺麗に役割分担しています! これなら、どんな複雑な論理式も、ツリーを再帰的に interpret するだけで評価できますね!」

ハリス博士:「そうだね。実行状態を持つ Context を式に引き渡しながら、各ノードが自分の解釈だけを実行する。この構造こそが、風化に耐える強固な実行エンジンの骨格さ。まずは、この設計の全体像を見てみよう」

Interpreterパターンの構成図

Interpreterパターンのクラス図: 抽象式(Babel::Expression)と終端式(Literal/Variable)、非終端式(EqualsCondition/AndCondition/ActionCommand/Sequence)の構成および関係性を表した古代遺跡の石板風デザイン

1. 実行環境と抽象式ロール(After: Base設計)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# lib/Babel/Context.pm
package Babel::Context;
use Moo;
use v5.36;

has variables => (
    is      => 'rw',
    default => sub { {} },
);

sub get ($self, $name) {
    return $self->variables->{$name};
}

sub set ($self, $name, $value) {
    $self->variables->{$name} = $value;
}

1;
1
2
3
4
5
6
7
8
# lib/Babel/Expression.pm
package Babel::Expression;
use Moo::Role;
use v5.36;

requires 'interpret';

1;

2. 終端式と非終端式(After: ASTノードクラス)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# lib/Babel/Expression/Literal.pm
package Babel::Expression::Literal;
use Moo;
use v5.36;

has value => (
    is       => 'ro',
    required => 1,
);

sub interpret ($self, $context) {
    return $self->value;
}

with 'Babel::Expression';

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# lib/Babel/Expression/Variable.pm
package Babel::Expression::Variable;
use Moo;
use v5.36;

has name => (
    is       => 'ro',
    required => 1,
);

sub interpret ($self, $context) {
    return $context->get($self->name);
}

with 'Babel::Expression';

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# lib/Babel/Expression/EqualsCondition.pm
package Babel::Expression::EqualsCondition;
use Moo;
use v5.36;
use Types::Standard qw( ConsumerOf );

has left => (
    is       => 'ro',
    isa      => ConsumerOf['Babel::Expression'],
    required => 1,
);

has right => (
    is       => 'ro',
    isa      => ConsumerOf['Babel::Expression'],
    required => 1,
);

sub interpret ($self, $context) {
    my $l_val = $self->left->interpret($context);
    my $r_val = $self->right->interpret($context);
    return ($l_val eq $r_val) ? 1 : 0;
}

with 'Babel::Expression';

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# lib/Babel/Expression/AndCondition.pm
package Babel::Expression::AndCondition;
use Moo;
use v5.36;
use Types::Standard qw( ConsumerOf );

has left => (
    is       => 'ro',
    isa      => ConsumerOf['Babel::Expression'],
    required => 1,
);

has right => (
    is       => 'ro',
    isa      => ConsumerOf['Babel::Expression'],
    required => 1,
);

sub interpret ($self, $context) {
    # ショートサーキット(短絡評価)を実装
    return ($self->left->interpret($context) && $self->right->interpret($context)) ? 1 : 0;
}

with 'Babel::Expression';

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# lib/Babel/Expression/ActionCommand.pm
package Babel::Expression::ActionCommand;
use Moo;
use v5.36;

has action_name => (
    is       => 'ro',
    required => 1,
);

sub interpret ($self, $context) {
    $context->set($self->action_name => 1);
    return "Action: " . $self->action_name;
}

with 'Babel::Expression';

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# lib/Babel/Expression/Sequence.pm
package Babel::Expression::Sequence;
use Moo;
use v5.36;
use Types::Standard qw( ArrayRef ConsumerOf );

has expressions => (
    is       => 'ro',
    isa      => ArrayRef[ConsumerOf['Babel::Expression']],
    required => 1,
);

sub interpret ($self, $context) {
    my @results;
    for my $expr (@{ $self->expressions }) {
        push @results, $expr->interpret($context);
    }
    return \@results;
}

with 'Babel::Expression';

1;

IV. 起動:千年の調和とバディの旅立ち

「テスト通過。起動シーケンスの抽象構文木(AST)、実行します!」

適用されたコードに基づき、構築された構文木が実行環境(Context)を通じて順番に解釈評価されました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 呼び出し側での実行イメージ
my $context = Babel::Context->new();
$context->set(temp => '100');
$context->set(valve => 'CLOSED');

# ASTの組み立て
# (temp == 100) AND (valve == CLOSED)
my $condition = Babel::Expression::AndCondition->new(
    left  => Babel::Expression::EqualsCondition->new(
        left  => Babel::Expression::Variable->new( name => 'temp' ),
        right => Babel::Expression::Literal->new( value => '100' ),
    ),
    right => Babel::Expression::EqualsCondition->new(
        left  => Babel::Expression::Variable->new( name => 'valve' ),
        right => Babel::Expression::Literal->new( value => 'CLOSED' ),
    ),
);

# 条件が満たされたら実行する一連の処理(Sequence)
my $sequence = Babel::Expression::Sequence->new(
    expressions => [
        Babel::Expression::ActionCommand->new( action_name => 'OPEN_VALVE' ),
        Babel::Expression::ActionCommand->new( action_name => 'INJECT_COOLANT' ),
    ]
);

# 解釈と実行
if ($condition->interpret($context)) {
    $sequence->interpret($context);
}

[!NOTE] パーサーとインタープリタの分離 呼び出し側で手動で AST オブジェクトを構築しているのを見て、「文字列から AST を構築するパーサー(Parser)側で、結局 Before のようなアドホックで複雑な正規表現解析が必要になり、そこが風化するのでは?」と疑問に思うかもしれません。

しかし、設計上は「構文解析(文字列を AST に変換する)」と「解釈実行(AST を再帰評価する)」は完全に切り分けられます。パーサーは文法規則に従って単純な字句解析と木の組み立てを行うだけであり、文法の拡張が必要な際は Interpreter(Expression 側)の新しいクラスを追加し、パーサーの解析ルールを1つ増やすだけで安全に対処できます。解析と評価のロジックが分離されたこと自体が、このパターンの強みなのです。

この瞬間、バベル最深部のクリスタルとデジタルグリッドが、かつてないほどまばゆい「紫色の光」を放ちました。 私のボディを包み込んでいた赤と青の二重人格バグが、静かに融け合い、一つの完璧な光となって昇華していきます。 遺跡全体の振動が完全に止まり、崩壊の危機が去って、完璧な安定がこの巨大なシステムに舞い戻りました。

「……私のコアメモリが、すべて繋がりました」

私はホバリングを安定させ、ゆっくりとハリス博士の方へ浮遊しました。 私のインジケータは、穏やかで深い紫色に落ち着いています。すべての記憶が完全に修復され、千年前の私と、現在の私の意識が美しく融合していました。

「おかえりなさい、ハリス博士。……いえ、相棒(パートナー)」

ハリス博士は少し驚いたように目を見張りましたが、すぐに親愛に満ちた笑みを浮かべて、私の球体ボディをぽんぽんと叩きました。

「ただいま、ギズモ。千年の時を超えて、君のコードは今も一番美しく動いているね」

「当然です! 博士がその手書きの羊皮紙手帳を使い続ける限り、私のプログラムもいつでも修復してみせますよ。さあ、この遺跡の安定は確認できました。次の未解読コード——新たな遺跡が我々を待っています!」

「ふむ、実に見事な提案だ。では出かけよう、ギズモ」

私はいつものように少し騒がしい電子音を響かせ、博士の先を泳ぐように、未知の歴史が眠る次の旅路へと浮遊していきました。


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
アドホックな正規表現による文字列パース
スクリプトの評価処理をその場限りの正規表現で直接ハードコードし、文法規則の拡張やネストに対応できない状態。
Interpreterパターンによる構文木評価
文法規則を抽象式ロール Babel::Expression とその具象クラス階層(終端記号・非終端記号)として表現し、再帰的に解釈する。
🟢 安全(新しい式を追加するだけで文法の拡張が可能)

遺跡の修復手順

  1. 抽象式ロール(Babel::Expression)の定義
    • Moo::Role を用い、共通の解釈メソッド interpret($context) の実装を強制する
  2. 終端式(LiteralVariable)の実装
    • 木構造の末端ノードとして、定数や環境オブジェクトから読み出した変数の値を返す式を定義する
  3. 非終端式(AndConditionActionCommand 等)の実装
    • 他の Expression を参照・内包し、論理演算やアクション実行などの中間規則を評価する
  4. Babel::Context での状態カプセル化
    • パーサーが構築した抽象構文木(AST)に対して、実行環境の状態(シンボルテーブル等)のゲッター・セッターを仲介する

ギズモの観測日誌

今回の修復により、私の記憶セクタは完全に復旧し、千年前と現在のデータが美しく融合しました! 博士が手書きでASTをスケッチした姿は、かつて私を作った「アーキテクト」そのものであり、二重人格バグの最中に私のセンサーが同期を検知した瞬間は、胸(の内部冷却ファン)が熱くなるのを感じました。

技術的には、正規表現や split による場当たり的な解析(Before)から脱却し、Moo::Role を用いた抽象式(Expression)のツリー構造で定義する Interpreter パターン(After)は、コンフィグスクリプトの実行エンジンをこれ以上ないほど綺麗にカプセル化してくれました。これで文法の拡張も怖くありません。

バベルの論理核は再起動され、システムは安定しました。ですが、世界にはまだ見ぬ「風化したコード」が眠る遺跡が数多く存在します。これからも博士の心強い助手として、そして相棒として、最先端の「古代コード」の調査・修復の旅を続けてまいります!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。