Exporter を使ったモジュールの継承

おまえは何を言っているんだみたいな話だったらすみません。


たとえば,foo() という関数を export している ModA というモジュールがあるとします。

package ModA;

use strict;
use warnings;

use base qw( Exporter );

our @EXPORT = qw( foo );

sub foo {
    warn sprintf '%s::foo called', __PACKAGE__;
}

1;

こいつをたとえば

#!/usr/bin/perl

use strict;
use warnings;

use ModA;

foo();

のように使うと,

ModA::foo called at ModA.pm line 12.

のように出ます。

ここまでは当たり前。

use ModB したら ModA::foo() を呼び出せるようにしたい

んで,use ModB ってしたら foo() って呼び出せて,それは実際には ModA::foo() が呼び出されるようにしたいとします。

なんとなく継承すればいいのかな,と思って*1

package ModB;

use strict;
use warnings;

use base qw( ModA );

1;

のように use base qw( ModA ); してやればいいんじゃないかなと思って書くと,

#!/usr/bin/perl

use strict;
use warnings;

use ModB;			# instead of 'ModA'

foo();

こいつが,

Undefined subroutine &main::foo called at test.pl line 8.

のようにうまくいきません。


というのは use base@ISA を使った継承というのは,あくまで OO で Perl を使うとき(たとえば $instance->foo() とか KLASS->bar() とか)のためのものであって,今回やりたかったことというのは実は継承じゃないからなんですね。

ModBimport() を定義してみる

Exporter を使うとパッケージ名(ModA::)を省略して foo() と呼び出すことができるのは,use したときに import() 関数が呼び出されて,そこで main:: 名前空間*2に関数を登録するからで……みたいな「むつかしい話」は他にゆずるとして。


ともかく,import() という関数がキーポイントみたいだから,ModBuse したときに ModAimport() を呼び出したらいいんじゃないか。と思って下記のように書いてみます。

package ModB;

use strict;
use warnings;

use base qw( ModA );

sub import {
    my $pkg = shift;
    return ModA->import(@_);
}

1;

ところが,これでもうまくいきません。

というのは,ModA->import() したときに何がおきるかというと,それを実行したところの名前空間ModB::)に関数を導入するからなんですね。ほんとは(今回の場合)main:: という名前空間に導入してほしかったんですけど,それは(コールスタック上)「もう一段上のレベル」だったのです。

Exporter::export_to_level() が使える

で,その「もう一段上のレベル」の「名前空間に関数を導入する」ためには,Exporterexport_to_level() といういかにもな名前の関数を利用します。

今回の例だと,

package ModB;

use strict;
use warnings;

use base qw( ModA );

use Exporter;

sub import {
    my $pkg = shift;
    return Exporter::export_to_level('ModA', 1, @_);
    # または ModA->export_to_level(1, @_);
}

1;

のようにします。ここで 1 というのが引数に出てきてますけど,これが 0 の場合さきほどの例と同じように ModB:: という名前空間がターゲットになります。で,1 にすると,import() を呼び出したレベル……今回でいうと main:: ……に関数を(Exporter が)export してくれる,というわけです((むろん 2 以上の export level を指定することもできます。import() 関数に直書きするんじゃなくて他のサブ関数を経由する場合とか。))。

このコードで先ほどのスクリプトを実行すると,

ModA::foo called at ModA.pm line 12.

のように無事 ModA::foo() が呼び出されます。


ちょっと余談ぽくなりますが,さきほどいったように,これは「継承」じゃないので,ModB

use base qw( ModA );

という部分は実はいりません。ただ,その代わりに

use ModA;

としておかないとだめですけど。

実は Exporter を継承していない場合

これでオッケーと思いきや,ModA の実装によっては

Can't locate object method "export" via package "ModA"
    at /usr/lib/perl5/5.8.8/Exporter/Heavy.pm line 217.
BEGIN failed--compilation aborted at test.pl line 6.

のように怒られる場合があります。

これは,ModA@ISAExporter が含まれない場合です。たとえば,ModA の実装が

package ModA;

use strict;
use warnings;

use Exporter 'import';			# instead of 'use base qw( Exporter );'

our @EXPORT = qw( foo );

sub foo {
    warn sprintf '%s::foo called', __PACKAGE__;
}

1;

のようになっている場合。この場合でも ModA::import()Exporter::import() を利用してくれるので,普通の export としてきちんと働きます。

しかし,さきほどの export_to_level() という関数は内部で ModA->export() という関数を利用していますので,それがみつかんないよ,と怒られるわけです。


対策としては,無理やり ModAexport() 関数を定義してやればよろしい。

package ModB;

use strict;
use warnings;

use ModA;

use Exporter;
*ModA::export = \&Exporter::export;

sub import {
    my $pkg = shift;
    return Exporter::export_to_level('ModA', 1, @_);
}

1;

これでうまくいくようになります。

厳密には ModAexport() という(別内容の)関数が定義されていた場合に備えて,

sub import {
    my $pkg = shift;

    use Exporter;
    local *ModA::export = \&Exporter::export;

    return Exporter::export_to_level('ModA', 1, @_);
}

のようにしたほうがいいかも。

おわりに

まあ実は Exporter を使わなくても,自分で適切な import() を書けば関数の export はできます。自分で書いてみるなり,Exporter とか Exporter::Lite とかのソースを読むなりすると,シンボルテーブルとかそのへんの知識も深まりますし,上で書いた内容ももっとよくわかると思います。

*1:他言語出身だとこう思っちゃいそうな気がします。他言語に限らないか。

*2:厳密に言うと名前空間じゃないですよね。シンボルテーブル?stash?