モダンPerlの世界へようこそ

第2回mro:次のメソッドはどこ?

モダンなクラス/オブジェクトのあり方は?

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

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の世界では、あらたに別の手法を利用したメタモデルの実装が検討されるようになります。

Class::C3の登場

その努力は、2005年8月にPerl6::MetaModel[3]という名前で結実しました。詳細については長くなるので次回に譲りますが、実装を担当したスティーヴン・リトル(Stevan Little)氏は、そこで得られた知見をPerl 5に還元するための努力を始めます。

そのひとつがその数日後にリリースされたClass::C3でした。このClass::C3を使うと、先の例はこのように書き直せます。

use strict;
use warnings;
use Class::C3;

package ClassA;
sub initialize { print "A"; }

package ClassB;
use base qw(ClassA);
sub initialize { print "B"; shift->next::method; }

package ClassC;
use base qw(ClassA);
sub initialize { print "C"; shift->next::method; }

package ClassD;
use base qw(ClassB ClassC);
sub initialize { print "D"; shift->next::method; }

package main;
ClassD->initialize;   # DBCA

メソッドの解決順序が安定するClass::C3の登場は、NEXTの不安定さに悩まされていた開発者にとってはまさに福音でした。マット・トラウト(Matt S Trout)氏率いるDBIx::Classが2005年11月リリースのバージョン0.04からNEXTを捨ててClass::C3に切り替えたのを皮切りに、Catalystの世界でも同時期から少しずつClass::C3に乗り換える動きが起こってきています(最終的な切り替えはまだ済んでいませんが、次期バージョンの5.80系列で完全に切り替わることになっています⁠⁠。

mroプラグマの導入

また、2007年(Perl 20歳)にリリースされたPerl 5.10では、このClass::C3(や、Perl 6のオブジェクトモデル)をもとにしたmro(これはMethod Resolution Order「メソッド解決順序」の頭文字を並べたものです)というプラグマがコアモジュール入りしました。このプラグマを使うと、先の例はこのように書けます。

use strict;
use warnings;

package ClassA;
use mro 'c3';
sub initialize { print "A"; }

package ClassB;
use base qw(ClassA);
use mro 'c3';
sub initialize { print "B"; shift->next::method; }

package ClassC;
use base qw(ClassA);
use mro 'c3';
sub initialize { print "C"; shift->next::method; }

package ClassD;
use base qw(ClassB ClassC);
use mro 'c3';
sub initialize { print "D"; shift->next::method; }

package main;
ClassD->initialize;   # DBCA

ここではそれぞれのクラスの中で明示的にC3のアルゴリズムを利用するように指定しましたが、この例に限って言えば、'c3'の指定を外しても期待通りの動作をします(この場合、内部的には深さ優先の探索が行われますが、SUPERやNEXTのようにメソッドを取りこぼすことはありません⁠⁠。

互換性のためにMRO::Compatを

ただし、このmroプラグマは5.10系列(正確にはPerl 5.9.5)以降でしか利用できません。逆に、5.10系列以降ではわざわざClass::C3を使う必要はありません。

そのため、過渡期の対策として、現在はMRO::Compatというモジュールを利用することが推奨されています(このモジュールは、Perlのバージョンを見て、コアのmroプラグマを使うか、従来のClass::C3に対応した仮のmroを使うかを判断してくれます⁠⁠。

先の例であれば、use mro 'c3';の前にuse MRO::Compat;を追加するだけでPerl 5.9.5未満でもC3探索を実行できます。また、C3探索を行わないのであれば、use mro 'c3';の指定を省略することもできます。

use strict;
use warnings;
use MRO::Compat;

package ClassA;
sub initialize { print "A"; }

package ClassB;
use base qw(ClassA);
sub initialize { print "B"; shift->next::method; }

package ClassC;
use base qw(ClassA);
sub initialize { print "C"; shift->next::method; }

package ClassD;
use base qw(ClassB ClassC);
sub initialize { print "D"; shift->next::method; }

package main;
ClassD->initialize;   # DBCA

Class::C3::XSによる高速化

また、mroプラグマ開発の副産物として、ブランドン・ブラック(Brandon Black)氏はClass::C3を高速化するClass::C3::XSというモジュールをリリースしました。このモジュールをインストールしておくとClass::C3が自動的に高速なXSバージョンを利用するようになるので、Perl 5.9.5未満の環境でコンパイラが使えるならぜひインストールしておきたいところです。

MRO::Compat/Class::C3を使うときの注意点

よいこと尽くめに見えるC3探索ですが、実際のコーディングではいくつか注意が必要な点もあります。

まず、Class::C3は初期化時にクラスの構造を解析するため、初期化が済んだあとでメソッドを追加したり、継承ツリーをいじったりすると、期待した動作が得られないことがあります(Class::C3の適切な初期化関数を呼ぶ必要があります⁠⁠。

また、NEXTや、とりわけ疑似クラスのSUPERとは併用しないほうが無難です(※4)。大規模なシステムでNEXTからClass::C3への移行が困難な場合は、Class::C3::Adopt::NEXTというCPANモジュールを利用してみるとよいでしょう。

複数のプラグイン、コンポーネントで引数を使い回す場合、あとから引数が増えても対応できるよう「@_」を渡すのが定石ですが、引数の受け取り方によってはコードが汚くなるので、Catalyst::Manual::ExtendingCatalystでは次のような書き方をすることが推奨されています。

sub foo {
    my $self = shift;
    my ($bar, $baz) = @_; # ←これが実際に引き回したい引数
    $self->next::method(@_);
}

ただし、Class::C3やMRO::Compatが用意するnextなどはSUPERとは異なり実際のクラスですので、このような書き方をしても問題なく動作します。

sub foo {
    my ($self, $bar, $baz) = @_;
    next::method(@_);
}

NEXTと違って、next::methodは実際に次のメソッドがない場合はエラーになります。次のメソッドがあるかどうかわからない場合はmaybe::next::methodを使います。

use strict;
use warnings;
use MRO::Compat;

package ClassA;
sub initialize { print "A"; maybe::next::method(@_); }

package ClassB;
use base 'ClassA';
sub initialize { print "B"; maybe::next::method(@_); }

package main;
ClassB->initialize;

本当に多重継承する必要はあるのでしょうか

さて、ここまでPerlの多重継承のあり方がこの数年間でどう変化してきたかを見てきましたが、そもそもクラスを拡張するために多重継承をする必要はあるのでしょうか。

次回はその命題にモダンPerlがどのように答えてきたかを見ていきましょう。

おすすめ記事

記事・ニュース一覧