Perl Memo

Perl についてのメモです。

はじめに

tie により遅延初期化を実現する

はじめに - 遅延初期化

初期化にコストのかかるオブジェクトがあり、かつそれを必ずしも毎回使用する必要のない場合、必要となった時にはじめて初期化する、といったスタンスをとることができます。

# $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() );

しかし、この手法にはいくつかの改善すべき点があります:

  • 上の例のように変数の所有を関数側にすると寿命管理がしづらくなる。
  • (逆に)関数呼び出し側で変数を所有しそれを生成関数に引き渡すスタイルをとると、「変数名」「生成関数名」のふたつの識別子を管理することになりあまりよろしくない。
  • (当然のことではあるが)関数呼び出しが多発し視覚的な区別がしづらくなる。

まぁ、わざわざ関数を経由させることなく記述したいものです。

Proxy パターン

$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)へのアクセスコストがかかります(これに関しては生成関数を経由した場合も同じ。しかし、こちらはメソッドコールなので多少割高)。

tie を使った解決法の模索

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

注意:このコードには問題があります。(後述)

とりあえず、一体このコードは何をやろうとしているのか、というと:

  1. tie の行。変数 $num をクラス Phantom でもって tie する。引数として $num へのリファレンスを与える。
    1. 結果、Phantom::TIESCALAR が呼ばれる。
    2. Phantom::TIESCALAR は、渡されたリファレンスにクラス Phantom を bless して返す。以下、この戻り値を $tied とし、また tied object とも呼ぶことにする。
  2. print の行。$num->{foo} を表示しようとする。
    1. 引数 $num->{foo} が評価される。
    2. $num をデリファレンスするために、$num 自体の値が評価される。
    3. $num はタイ変数なので、関連づけられたオブジェクトの FETCH メソッドを呼び出す。すなわち、$tied->FETCH が呼ばれる。
    4. Phantom::FETCH では:
      1. まず変数 $ref$num へのリファレンスを格納する。
      2. 次に $ref のリファレント(つまり $num)に対して untie をかける。これにより $num への tie が解除される。
      3. 最後に $ref のリファレント(つまり $num)に無名ハッシュ { foo => 1 } を代入する。
      4. 戻り値は最後に評価した値である $num、即ち { foo => 1 }
    5. 結局、$num->{foo} の値として { foo => 1 } でのキー foo の値である 1 が返る。
    6. 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]

untie 問題とその解決法

何がまずかったのでしょう? 実は、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 化してみました。

なぜ require に 1 を返させるのか

標準モジュールなど中身を覗いてみるとわかるのですが、ファイルの終わりあたりや __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 の話題というわけではないのですが、かの有名な 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|| に変えて優先順位を上げる
  • 否定をとって andor に、orand にする

あたりがありそうです。|| を使うのは何となく不格好なので、否定をとってみましょう。

# $year年 $mon月の末日 $lastday を求める
$lastday = $mon == 2 ? 29 - ($year % 4 != 0 or $year % 400 != 0 and $year % 100 == 0) 
                     : 31 - ($mon - 1) % 7 % 2;

…だからどうした、という気もします。

謝辞

新しい発見をいつもさせていただいております。感謝です。

このメモの記事のいくらかは、この ML で話題になったことをまとめたものです。

今後の予定

こんなネタを書くかも。

また、[perl-oo] のまとめとして、以下のことを書くかも。

LoveVector : klm (PaGe) <klm at okowa dot org>