Perl についてのメモです。
初期化にコストのかかるオブジェクトがあり、かつそれを必ずしも毎回使用する必要のない場合、必要となった時にはじめて初期化する、といったスタンスをとることができます。
# $obj を使う場面で ...
initialize($obj) unless defined $obj; # 未初期化ならここで初期化する
do_something($obj);
# ...
しかし、このような初期化コードを毎回 $obj を使う場面で書くのはまどろっこしいわけで、これをどうにかして隠蔽しようと考えます。たとえば、内部で未初期化なら初期化を行うような生成関数を経由すれば隠蔽できます:
# $obj インスタンスを返す
my $obj;
sub get_obj
{
initialize($obj) unless defined $obj;
return $obj;
}
# ...
do_something( get_obj() );
しかし、この手法にはいくつかの改善すべき点があります:
まぁ、わざわざ関数を経由させることなく記述したいものです。
$obj が何かのクラスのインスタンスで、内部構造にはタッチせずにメソッドコールしか行わないのであれば、virtual proxy(Proxy パターン, GoF本)を利用するという手もあります。簡単に言えば、別のクラスで包んでしまうという手法です:
package RealSubject;
sub new { print '(construct)' ; bless { foo => 1 }, shift }
sub foo { shift->{foo} }
package Proxy;
sub new { bless \( my $s ), shift }
sub DESTROY {}
sub AUTOLOAD
{
my $self = shift;
( my $method = $AUTOLOAD ) =~ s/.*:://;
( $$self ||= new RealSubject )->$method(@_);
}
package main;
my $subj = new RealSubject; #=> '(construct)'
my $proxy = new Proxy; #=>
print $subj->foo; #=> 1
print $proxy->foo; #=> '(construct)', 1
すべての RealSubject メソッドは、Proxy を経由して呼び出されています。また、最初のメソッド呼び出し($proxy->foo)が起こるまで、RealSubject インスタンスは作成されていません(ので目論見通りです)。AUTOLOAD により、すべてのメソッドを横流しすることに成功しています。
このソリューションによって、「変数」の見た目を崩すことなく初期化が可能になりました。しかし、初期化したいものがオブジェクトではなく単なるスカラーだったりハッシュだったりする場合には適用できません。また、メソッドコールごとに中間オブジェクト(Proxy)へのアクセスコストがかかります(これに関しては生成関数を経由した場合も同じ。しかし、こちらはメソッドコールなので多少割高)。
NOTE: 以下の内容は perl 5.8.0 以上向け。
さて、ここで、tie が使えないかどうか考えてみましょう。tie を用いることにより、取得や代入などの変数への干渉をトラップできます。よって、これらの干渉がはじめてあった時、ひらたくいえば、変数に「触った」時を初期化タイミングとして使うことができます。また、タイ変数は untie すれば何事も無かったかのようにもとの変数に戻ります。この機構はなかなかよさそうです。つまり、変数に「触った」時、即ち FETCH や STORE などのメソッドが呼ばれた時点で自らを初期化します(遅延初期化)。さらに、自らを untie することにより、以降のアクセスに tie が関与しません(中間オブジェクトへのアクセスコストの排除)。とりあえず、やってみましょう。
package Phantom;
sub TIESCALAR { my ($class, $self) = @_ ; bless $self => $class }
sub FETCH { my $self = shift ; untie $$self ; $$self = { foo => 1 } }
tie my $num, 'Phantom', \$num;
print $num->{foo}; #=> 1
注意:このコードには問題があります。(後述)
とりあえず、一体このコードは何をやろうとしているのか、というと:
tie する。引数として $num へのリファレンスを与える。 bless して返す。以下、この戻り値を $tied とし、また tied object とも呼ぶことにする。$num->{foo} を表示しようとする。 $num->{foo} が評価される。$tied->FETCH が呼ばれる。untie をかける。これにより $num への tie が解除される。{ foo => 1 } を代入する。{ foo => 1 }。$num->{foo} の値として { foo => 1 } でのキー foo の値である 1 が返る。てなかんじです。ごちゃごちゃしていますが重要な点は、単なるスカラーを、遅延初期化しようとしていること、そして、初期化された後は余計なコードは一切絡まなくさせようとしている点です。
しかしこのコードは、遅延初期化については実現できているのですが、余計なコードを絡ませない試みについては実現できていません。ためしに、最後の行のあとに print tied($num) ? 'tied' : 'not tied'; などとしてみると tied と表示されます。つまり、untie されていません。
NOTE: 5.6.1 以前の perl を利用していると、not tied と表示されます(つまり、untie されています)。これは perl のバグによるもので、5.8.0 では fix されています。参考 - [perl-oo:0667]
何がまずかったのでしょう? 実は、FETCH や STORE などの中では $num はタイ変数ではないかのように振る舞うのです(内部で MAGIC ビットを落とされている)。よって、untie しても何も起こりません。正直 Perl の範囲内でこれを解決する方法は思いつきませんでした。が、Inline::C を使えば何とかならないこともないようです。以下のような関数を使えばよいそうです。参考 - [perl-oo:0669]
void magical_on(SV* sv) { SvMAGICAL_on(SvRV(sv)); }
これは、(リファレントの)落とされた MAGIC ビットを立てる関数となります。これを untie する前にかませばいいので、FETCH メソッドは以下のようになります。
sub FETCH
{
my $self = shift;
magical_on($self);
untie $$self;
$$self = { foo => 1 };
}
これで OK です。なんか危険臭いですが…(危険であるとわかる方はお教え下さい)。あと、「それなら全部 C で書こうよ」とか無しね:p。
あとはこれを利用しやすいようにすれば幸せになれます。tie されて初期化待ちになっている状態を Phantom 状態と名付けることにします。
Phantom 化するための関数 phantomize をどう作ればいいでしょうか? 前の例では、tie をするのに、変数 $num とそのリファレンスと両方を扱わなければなりませんでしたが、リファレンスから本体は導出できるので phantomize に渡すのはリファレンスのみで OK です。例示:
phantomize(\$num);初期化方法は変数により違ってくると思うので、これも無名関数で渡してしまいましょう。この初期化関数の第一引数は、初期化したいもののリファレンスということににしましょう。
phantomize( sub { ${+shift} = { foo => 1 } }, \$num );さらにプロトタイプを使って遊びましょう。プロトタイプ & を使えば、無名関数を map や sort のように sub を書くことなく渡せます。sub phantomize(&$) として、以下のようにできます:
phantomize { ${+shift} = { foo => 1 } } \$num;もういっちょ。perl 5.8.0 以上じゃないと使えませんが、プロトタイプ \[$@%*] というのがあります。これを使えば、暗黙的にスカラー、配列、ハッシュ、型グロブのリファレンスを渡すことができます。そうするとわざわざ \ を使ってリファレンスを生成する必要がなくなります。sub phantomize(&\[$@%*]) として、以下のようにできます。これで最終形です:
phantomize { ${+shift} = { foo => 1 } } $num;最終形ではリファレンスを作らなくても良いので、自然なところに my を置くことができます:
phantomize { ${+shift} = { foo => 1 } } my $num;最初と比べると大違いですね。いや、違う、としか言ってませんって!
関数 phantomize では第二引数が何のリファレンスなのかを調べ、リファレントをそれに合わせたクラス(Tie::Phantom::Scalar, Tie::Phantom::Array, ...)で tie してやります。それぞれのクラスは、メソッドが呼ばれると自らを untie し、その後コンストラクタで渡された初期化関数でもって初期化し、最後に行われたメソッドに対応する作業を行います。
以上の作業をモジュール化したものが以下の Tie::Phantom です。
現在のところいくつか不都合があります。以下のような「触り」方をすると、うまくいきません。どうにかならんもんかな。
$#array = $scalar; (配列の STORESIZE。サイズ変更が反映されない)@array = (); (配列の CLEAR。perl が落ちる)%hash = (); (ハッシュの CLEAR。perl が落ちる)($key, $value) = each %hash; (リストコンテキストでの each。初回の $value が空になる)やっぱり一から MAGIC いじりしなきゃいけないのかなぁ。くそう。
perl v5.6.0 から、lvalue 属性がサポートされました。これを使えばサブルーチン、さらにはメソッドコールが呼び出しが左辺値として扱えるため、いわゆる property っぽいことができるようになります。
package Object;
sub new { bless { foo => 1 } => shift }
sub foo : lvalue { shift->{foo} }
package main;
my $obj = new Object;
$obj->foo = 5;
print $obj->foo;
しかし、setter/getter に処理を挟もうとするとこれではダメで(setter のみの場合だけは対応可能)、代入をトラップするためにタイ変数をこしらえる等の処理が必要になります。つまり、set_foo と get_foo を用意し:
sub get_foo { print ' *get_foo* ' ; shift->{foo} }
sub set_foo { print ' *set_foo* ' ; $_[0]->{foo} = $_[1] }
アダプタとしてタイスカラーを用意して:
package Tie::Accesser;
sub TIESCALAR { my $class = shift ; bless shift, $class }
sub FETCH { my $self = shift ; $self->[1]->($self->[0]) }
sub STORE { my $self = shift ; $self->[2]->($self->[0], shift) }
foo を書き換えれば:
sub foo : lvalue
{
my $self = shift;
tie my $tiescalar, 'Tie::Accesser' => [ $self, \&get_foo, \&set_foo ];
$tiescalar;
}
まぁ何となく期待していることができるようになります。use property foo => { set => sub { ... }, get => sub { ... } } な記法ができるように package 化してみました。
標準モジュールなど中身を覗いてみるとわかるのですが、ファイルの終わりあたりや __END__ の前当たりに 1; と書いてあるものが多いです。ラクダ本にも require で読み込まれるファイルは初期化コードが成功したことを示すために、最後に真を値として返さなければならない
とあるとおり、真値を返さなければならず、また、1; と書くのが習慣になっているわけですが、なぜ 1 なのかな、と思う方は、以下のスクリプトを実行してみれば幸せになれるかも。
unless($main::Once)
{
$main::Once = 1;
print __FILE__, "\n";
print require( __FILE__ ), "\n"; #=> 100
print require( __FILE__ ), "\n"; #=> 1
}
100;
というわけで、1 じゃないとまずいわけです。最後に __PACKAGE__ としておいて、my $obj = ( require Object )->new なんてやって遊ぶことは危険なわけです。しょぼーん。
1 ではなく他に何か変わったものを返させているモジュールもあります。探してみたところ、Michael G Schwern さんがけっこう出しているようです。
他にやってる方はいないのかなぁ…。
大崎さんのPerlメモでは、年月から末日を求めるプログラムが紹介されています。
# $year年 $mon月の末日 $lastday を求める $lastday = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$mon - 1] + ($mon == 2 and $year % 4 == 0 and ($year % 400 == 0 or $year % 100 != 0));
以下は、この中に含まれる、各月の日数を表すリストを何とかして無くそう、というあまり実りのなさそうな話です。別に Perl の話題というわけではないのですが、かの有名な Perl メモ関連の話題と言うことで:p。
リストの構成要素は 28(2月のみ) と 30 と 31 です。閏年関連の処理がありますからどのみち2月は別に扱う必要があるので、それを除く 11 個の要素が生成できればほぼ十分でしょう。なので、何とかして数列 (31, *, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) を生成するような式を作ることを考えます(* はワイルドカード)。いや、これの要素は 30 と 31 しかないのですから、(1, *, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1) などのようなもので十分です(別に 30 を足せばよい)。
さて、数列 (1, *, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1) を生成するにはどうすればいいか。何となくぼーっと眺めていると 1 と 0 が交互にでてきていることがわかりますので、2 の剰余を使えばうまくいきそうです。即ち、$mon % 2 です。しかし、これでは8月以降がうまくいきません。7月も8月も31日なので、ここでズレるんですね。ズレを無くすために折り返してみます。7 と 8 の間で折り返すのですから、$mon % 8 % 2 としてみます。
| (mod 2) | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
|---|---|---|---|---|---|---|---|---|
| 商 0 | 1月 | * | 3月 | 4月 | 5月 | 6月 | 7月 | |
| 商 1 | 8月 | 9月 | 10月 | 11月 | 12月 |
8月の値が 0 になってしまいました。頭を合わせるために、全体を1つずらします。即ち、($mon - 1) % 7 % 2 としてみます。
| (mod 2) | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
|---|---|---|---|---|---|---|---|
| 商 0 | 1月 | * | 3月 | 4月 | 5月 | 6月 | 7月 |
| 商 1 | 8月 | 9月 | 10月 | 11月 | 12月 |
アタマがそろいました。しかし今度は1月が 0、8月も 0 にマッピングされてしまっています(ちょうど得ようとしているものを反転したものです)。しかし、30 にこれを足すのをやめて、31 からこれを引けば何の問題もありません。というわけで、結局 31 - ($mon - 1) % 7 % 2 で事足ります。閏年を考慮したコードと合わせると最終的に以下のコードができあがりました。
# $year年 $mon月の末日 $lastday を求める
$lastday = $mon == 2 ? 28 + ($year % 4 == 0 and ($year % 400 == 0 or $year % 100 != 0))
: 31 - ($mon - 1) % 7 % 2;
閏年判定に使っている条件式で、or のほうが and よりも優先順位が低いためにカッコが一対必要になっています。これを解消するには:
or を || に変えて優先順位を上げるand を or に、or を and にするあたりがありそうです。|| を使うのは何となく不格好なので、否定をとってみましょう。
# $year年 $mon月の末日 $lastday を求める
$lastday = $mon == 2 ? 29 - ($year % 4 != 0 or $year % 400 != 0 and $year % 100 == 0)
: 31 - ($mon - 1) % 7 % 2;
…だからどうした、という気もします。
こんなネタを書くかも。
また、[perl-oo] のまとめとして、以下のことを書くかも。