モダンなクラス/オブジェクトのあり方は?
Perlではそもそもオブジェクトという考え方自体が,Perl 5(Perl 7歳)ではじめて登場した,後付けのものでした。また,その実装も非常におおらかなものだったため,より「本格的な」オブジェクト機構を備えた言語のユーザからはしばしば批判されてきました。
ただし,転んでもただでは起きないのがPerlハッカーたちのよいところ。そのような批判を糧に,「モダンPerl」の世界でもっとも激しく,多様に進化してきたのがこの分野です。
今回はそのようなクラス/オブジェクトの進化の一例として,クラスの継承とメソッドの解決順序にまつわる話題をまとめていきます。
継承によるクラスの拡張
伝統的なbaseプラグマを使ってクラスを拡張する場合,継承元と継承先に同名のメソッドがあれば継承先のメソッドだけが優先的に実行されます。
use strict;
use warnings;
package ClassA;
sub initialize { print "A"; }
package ClassB;
use base 'ClassA';
sub initialize { print "B"; }
package main;
ClassB->initialize; # B
ただし,場合によっては継承先のメソッドだけでなく,継承元のメソッドも実行したいことがあります。このような場合,伝統的にはPerl 5.002(Perl 9歳)のときに導入されたSUPERという疑似クラス(※1)を利用するのが常でした。
use strict;
use warnings;
package ClassA;
sub initialize { print "A"; }
package ClassB;
use base 'ClassA';
sub initialize { print "B"; shift->SUPER::initialize; }
package main;
ClassB->initialize; # BA
- ※1
疑似クラスといっても普通に使っている分にはピンと来ないかもしれませんが,要するにSUPER::initialize(shift)のような使い方はできない,ということです。詳しくはperldoc perlobjをご覧ください。
SUPERの問題点
ただ,このSUPERは継承元が増えたときに問題となることがあります。
use strict;
use warnings;
package ClassA;
sub initialize { print "A"; }
package ClassB;
sub initialize { print "B"; }
package ClassC;
use base qw(ClassA ClassB);
sub initialize { print "C"; shift->SUPER::initialize; }
package main;
ClassC->initialize; # CA
本当はClassAのinitializeも,ClassBのinitializeも呼びたかったのですが,ここでは先に継承したClassAのinitializeしか呼ばれていません。
もちろん継承の仕方を変えて,ClassCはClassBを,ClassBはClassAを継承するようにすればすべてのinitializeを呼ぶことはできますし,継承を使わず委譲を使って書き直す手もありますが,プラグインのようにクラスの数が変化する例のことを考えると,できれば継承元が増えても統一的に対応できる方法がほしいところです。
そこで登場したのがNEXTというモジュールでした。
NEXTの登場
NEXTは,2001年にYet Another Society(現在のThe Perl Foundation)からPerl 6の開発のために1年分の研究開発費を供与されたダミアン・コンウェイ(Damian Conway)氏が,Perl 6のオブジェクトモデル研究の一環として作成したものです(※2)。翌2002年にはPerl 5.8系列のコアモジュールにも採用されました。先の例は,NEXTを使うと,このように書き直せます。
use strict;
use warnings;
use NEXT;
package ClassA;
sub initialize { print "A"; shift->NEXT::initialize; }
package ClassB;
sub initialize { print "B"; shift->NEXT::initialize; }
package ClassC;
use base qw(ClassA ClassB);
sub initialize { print "C"; shift->NEXT::initialize; }
package main;
ClassC->initialize; # CAB
このように,NEXTを使うと,親クラスだけでなく,隣のクラスの同名メソッドも実行できるので,多彩な機能を持つ大きなクラスを,共通のインタフェースを持つ機能別のプラグインに分割することなどが簡単にできるようになります。
NEXTの問題点
ところが,このNEXTには,構成が複雑になるとメソッドの解決順が不安定になるという問題が残っていました。その問題がもっとも色濃くあらわれたのが,ゼバスティアン・リーデル(Sebastian Riedel)氏が2005年1月にリリースしたCatalystのプラグイン機構です。Catalystのプラグインは,セットアップ時に自らをコンテキストオブジェクトの継承ツリーに追加していくことで機能拡張するようになっているのですが,NEXTの制約のために,プラグインのロード順によっては正しく動作しないことがありました。その様子を簡単な例で確かめてみましょう。
use strict;
use warnings;
use NEXT;
package ClassA;
sub initialize { print "A"; shift->NEXT::initialize; }
package ClassB;
use base qw(ClassA);
sub initialize { print "B"; shift->NEXT::initialize; }
package ClassC;
use base qw(ClassA);
sub initialize { print "C"; shift->NEXT::initialize; }
package ClassD;
use base qw(ClassB ClassC);
sub initialize { print "D"; shift->NEXT::initialize; }
package main;
ClassD->initialize; # DBACA
このコードでもすでに一階層深いAがCより先に実行される(単独ではClassC→ClassAと進む初期化の順序が逆になってしまう)という問題があらわれていますが,ここでClassAから「shift->NEXT::initialize」の一文を取ってみてください。今度は「DBA」の順にしか実行されない(ClassCが無視される)のが確認できます。興味のある方はさらにClassBやClassCのuse base文をコメントアウトしてみてください。また違った結果があらわれます。
このような不安定さは,アプリケーションレベルであればなんとか許容できても,Perl 6という言語レベルではとうてい採用できるものではありません。Perl 6の世界では,あらたに別の手法を利用したメタモデルの実装が検討されるようになります。

