目次


洗練されたPerl: タイ変数 (Tied variables)

CPANモジュールを使ってのスカラー、配列、ハッシュのタイ変数の例

Comments

話を進める前に、Perl 5.005以降のバージョンをシステムにインストールしておいてください (入手先のリンクは、参考文献 に示してあります)。できれば、システムは最近の (2000年以降の) メインストリームUNIX (Linux、Solaris、BSDなど) を搭載したものがよいのですが、それ以外のオペレーティング・システムでも問題ないかもしれません。以下に示すコード・サンプルは、それ以前のPerlやUNIXあるいは他のオペレーティング・システムでも動作するかもしれませんが、そうした条件の下で何か問題が発生してうまく動かない場合には、読者が解決すべき練習問題だと考えてください。

タイ変数は、Perlプログラマーにとって必要不可欠なツールです。一番簡単なやり方は、Tie::ScalarTie::ArrayTie::Hash といったインターフェースを使用する既存のコードを再利用することですが、背景にある仕掛け (magic) を理解できれば、皆さん自身で工夫を凝らしてタイ変数を利用したいのか、あるいは単にその最適な使い方をしたいだけなのかに関係なく、役に立つことと思います。

ここでは、スカラー、配列、ハッシュの3種類の主だったタイ変数について見てみることにします。ファイル・ハンドルのタイは、複雑であり、より高度なテーマとなります。

本稿で取り上げるCPANモジュールは、CPANインターフェースでその実装を確認することができます。UNIXのプロンプトでcpanまたはperl -MCPAN -eshellとタイプすると、CPANのプロンプトが現れます。たとえば、Tie::Scalar::Timeout モジュールを調べたいときには、look Tie::Scalar::Timeoutとタイプします。すると、このモジュールの内容が表示されます。

変数を「タイする」というのは、どんな意味なのでしょうか。この場合の「タイする (tie)」という動詞は、「結合する (bind)」の同義語として使われています。変数をタイするというのは、基本的に、その変数の読み書きを行うための内部的なトリガーに関数を結合することです。すなわち、プログラマーである皆さんは、変数を使用する際に、何か特別なことを行うようPerlに指示できるということです。こうした単純な前提から出発して、タイ・インターフェースは、OOPの複雑さを手続き型インターフェースの背後に隠しながら、Perlにおけるオブジェクト指向の方法論として発展してきました。

スカラーのタイ

スカラー変数は、単純で必要不可欠な要素です。スカラー変数は、文字列、数値、未定義の値、別の変数への参照といったデータを1つだけ保持します。変数の前に $ を添えると、Perlは、それをスカラーとして扱います。スカラー変数を使うのは単純なことです。

リスト1. 通常のスカラー
my $a = 'Hello';
$a = 'there';
$a = 89.2;

スカラーのタイ変数を使うのも、同様に簡単なことです。たとえば、すばらしいTie::Scalar::Timeout モジュールの例を見てみます。

リスト2. スカラーのタイ
use Tie::Scalar::Timeout;
tie my $k, 'Tie::Scalar::Timeout', EXPIRES => '+2s';
$k = 123;
sleep(3);
# $k is now undef

最初のほうのtie() 関数を呼び出している部分は、変数$k が実際にはTie::Scalar::Timeout パッケージにタイされていることをPerlに伝えています。Perlは、背景で、Tie::Scalar::Timeout モジュール内のTIESCALAR() 関数を実行します (基本的に、これは、通常のオブジェクトに対してnew() を呼び出しているようなものです)。TIESCALAR() は、Tie::Scalar::Timeout 型のオブジェクトを返し、これが$k に代入されます。

この例でTie::Scalar::Timeout に指定されているパラメーターは、これを2秒後に期限切れにすることを指示しています。このモジュールは、この他に、何回か読み出しを行った後に期限切れにするなどのオプションも用意しています。上の例で生成された$k 変数を読み出すたびに、Tie::Scalar::Timeout モジュールのFETCH() メソッドが呼び出されます。

リスト3. Tie::Scalar::Timeoutの内部の動き
sub FETCH {
        my $self = shift;
        # if num_uses isn't set or set to a negative value, it won't
        # influence the expiry process
        if (($self->{NUM_USES} == 0) ||
           (time >= $self->{EXPIRY_TIME})) {
                # policy can be a coderef or a plain value
                return &{ $self->{POLICY} } if ref($self->{POLICY}) eq 'CODE';
                return $self->{POLICY};
        }
        $self->{NUM_USES}-- if $self->{NUM_USES} > 0;
        return $self->{VALUE};
}

スカラーのタイ変数に書き込みを行うたびに、そのSTORE() メソッドが呼び出されます。その他、スカラーには、通常必要とされることはありませんが、UNTIE() メソッドおよびDESTROY() メソッドも用意されています。

スカラー変数にかぎらず、タイされた変数は、すべて、その実際のデータをどこかに格納しておく必要があります。Tie::Scalar::Timeout の場合、われわれが使っている$k というスカラー変数は、背景ではハッシュになりますので、データは$self->{VALUE} に格納されます。Perlは、OOPに存在するものに非常によく似た一種のカプセル化を行うことで、この層の複雑さを、われわれの目から隠ぺいします。

上のコードは、変数$k の値が要求されるたびに、値が変わり得る ことを意味しています。したがって、独自のSchroedingerボックスがほしい場合には、Tie::Scalar::Timeout モジュールで、タイムアウトを0から100までの間のランダムな値にして、50秒おきに値を読み出せばよいというわけです。良好なランダム値発生器を使えば、タイムアウト値にしたがって、1か未定義が得られることになります。ここでは、プログラムの命令を実行するのに費やされる時間は無視できるものとしていますが、その時間によってわずかなバイアスが発生します。確かにrand(100) が50よりも大きいかどうかを調べてみることもできるのですが、面白みがありません。

リスト4. Schroedingerのタイムアウト
use Tie::Scalar::Timeout;
# the timeout will be between 0 and 99
my $random_timeout = rand 100; 
tie my $k, 'Tie::Scalar::Timeout', VALUE => 1, EXPIRES => "+${random_timeout}s";
sleep(50);
print 'The timeout ', ($k) ? 'did not happen' : 'happened', "\n";

猫を1匹用意し、タイムアウトになったら、それを撃つといったことは、読者の練習問題として残しておきます。

配列のタイ

配列は、スカラーよりも扱いが面倒です。配列は、スカラーの連続的な集合ですので、必要な機能も多くなります。配列には、TIEARRAY() (タイ配列用のnew() 同様のコンストラクタ)、FETCH()/STORE() (考え方は、スカラーのタイと同じだが、パラメーターが増える)、FETCHSIZE()/STORESIZE() (配列のサイズ管理用)、および ファイルをクローズしたり、出力をフラッシュするようなときに使用することのできるUNTIE() およびDESTROY() が用意されています。

タイ配列の場合には、スカラーのタイの場合に比べ、FETCH()STORE() のパラメーターが1個余計に必要となります。必要となるのは、配列の添え字です。FETCHSIZE()STORESIZE() は、それぞれ、スカラー (@ARRAY) および$#ARRAY = x を呼び出す際に使用されます。

DELETE() 関数とEXISTS() 関数は、それに対応するPerlのdelete() 関数およびexists() が必要とされる場合に実装する必要があります。

他にも、POP()PUSH()SHIFT()UNSHIFT()SPLICE()EXTEND() といった関数がありますが (最初の5つは、Perlの同じ名前の小文字の関数に対応しています)、通常、モジュール開発者は、Tie::StdArray から継承を行いますので、その場合、これらのメソッドは、すでに実装されていることになります。

たとえば、POP() は、最後の要素をFETCH() した後、STORESIZE(FETCHSIZE()-1) を行うものとして実装することもできるでしょう (配列のサイズを1小さくすることで、最後の要素を除去することになります)。当然のことながら、自分でPOP() を実装する場合、何を行っているのかを正確に把握している場合もあれば、正確に把握していない場合もあります。

独自にタイ配列を作成したい場合には、必ず、Tie::StdArray を継承するようにしてください (perldocのTie::Array参照)。関数は、すべて、すでに定義されており、変更したいものだけをオーバーライドすれば済みます。いちから作り直す必要はありません。余談ですが、タイ配列は、タイ変数の中で最も複雑な変数型であり、私のカウントでは、CPANで最も実装の少ない変数型です。ハッシュは、タイ配列に比べれば、全然難しくありません。(具体的な実装に興味のある方は、Tie::CharArray のソース・コードを見るとよいでしょう)

タイ配列の例として、CPANのTie::CharArrayモジュールを調べてみたいと思います。このモジュールを使えば、文字列は、文字の配列として、数値コードまたは1文字の文字列として扱うことができます。以下は、ドキュメントに示されている例です。

リスト5. 配列としての文字列
use Tie::CharArray;
my $foobar = 'a string';
tie my @foo, 'Tie::CharArray', $foobar;
$foo[0] = 'A';    # $foobar = 'A string'
push @foo, '!';   # $foobar = 'A string!'

これは、C/C++/Javaのプログラマーには極めてなじみ深い文法のはずです。

上の例の3行目を

tie my @foo, 'Tie::CharArray', 'a string';

と書くと、「読み出し専用値への変更を行おうとしました (Modification of a read-only value attempted)」というメッセージが出てエラーになります。それはそのはずで、'a string' は、変更することのできない定数文字列です。@foo 配列は、直接渡されてきた文字列を使い、値が代入されると、元の文字列を変更するようにしています。

Tie::CharArray は、実際、Perlのsubstr() 関数やpack()/unpack() 関数やsplit() 関数の詳細を忘れてしまうための非常に良い方法となっています。文字列の5文字目から28文字目までを変更する必要がある場合、substr()pack()/unpack()split() を利用する手もありますが、以下のように記述することもできます。

リスト6. 個々の文字のアドレッシング
use Tie::CharArray qw/chars/; 
$f = "jello is yellow"; 
my $chars = chars $f; 
foreach (5..28) 
{
 $chars->[$_] = "!";
};

どちらを選ぶか (組み込みの文字列操作関数にするかTie::CharArray にするか) は好みの問題ですが、リスト6のわかりやすさについては、ほとんど議論の余地がないでしょう。

ハッシュのタイ

今度のものは重宝できます。タイ・ハッシュは、タイ配列よりも簡単に記述でき、便利です。

タイ・ハッシュは、コンストラクタTIEHASH()、アクセス・メソッドFETCH()/STORE()、Perlのexists()delete() のような働きをするEXISTS()/DELETE()、ハッシュをクリアするためのCLEAR()、および配列内の巡回を行うためのFIRSTKEY()/NEXTKEY() を実装しています。Tie::StdHash パッケージ (Tie::Hashのperldocにある) を継承すれば、必要なメソッドがすべて定義されていますので、変更したいものをオーバーライドするだけで済みます。

ここでは、私が作成したTie::Hash::TwoWay モジュールのタイ・ハッシュの実装を紹介したいと思います。このモジュールは、内部的に2個のハッシュを管理しており、1個目のハッシュにデータが格納されると、自動的に2個目のハッシュに逆向きマッピングを作成します。たとえば、タイ・ハッシュTie::Hash::TwoWay に、dogというキーで値 ["Fido"] を、friendというキーでも値 ["Fido"] を代入した場合 (値は、配列の参照に入れる必要がある)、その同じタイ・ハッシュTie::Hash::TwoWay に、突如として、dogとfriendという値を (配列の形で) もつキーFidoが設けられることになります。以下は、ドキュメントに示してあるコードです。

リスト7. Tie::Hash::TwoWayの使い方
use Tie::Hash::TwoWay;
tie %hash, 'Tie::Hash::TwoWay';
my %list = (
            Asimov => ['novelist', 'scientist'],
            King => ['novelist', 'horror'],
           );
foreach (keys %list)                  # these are the primary keys of the hash
{
 $hash{$_} = $list{$_};
}
# these will all print 'yes'
print 'yes' if exists $hash{scientist};
print 'yes' if exists $hash{novelist}->{Asimov};
print 'yes' if exists $hash{novelist}->{King};
print 'yes' if exists $hash{King}->{novelist};

Tie::Hash::TwoWay は、Tie::StdHash モジュールを継承し、メソッドSTORE()FETCH()EXISTS()DELETE()CLEAR()FIRSTKEY() およびNEXTKEY() をオーバーライドしています。それに加えて、逆向きマッピングのキーを得るためのsecondary_keys() メソッドも定義しています。一次キーは$self->{1} に、二次キーは$self->{0} に格納されます。数値定数には、わかりやすくするために、PRIMARY、SECONDARYというシンボル名を付けてあります。

以下は、タイ化 / 継承 / 初期化の前段部分 (preamble) とドキュメント以外、モジュールに記述されているとおりのTie::Hash::TwoWay のコードです。リストは、関数単位で分割してあります。Tie::StdHash を継承していますので、インスタンスを得るためのTIEHASH() メソッドを定義する必要はありません。動作を変更したいメソッドを再定義しているだけです。

リスト8. STORE() 関数
sub STORE
{
 my ($self, $key, $value) = @_;
 my $val_array_ref;
 if (ref $value eq 'ARRAY')		# array refs can be recognized
 {
  $val_array_ref = $value;
 }
 else			# everything else gets converted to array refs
 {
  $val_array_ref = [ $value ];
 }
 # add the values in the passed array to the primary and secondary hashes
 foreach my $value (@$val_array_ref)
 {
  $self->{SECONDARY}->{$value}->{$key} = 1;
  $self->{PRIMARY}->{$key}->{$value} = 1;
 }
 return 1;
}

STORE() 関数は、一次配列と二次配列 (正常なマッピングと逆向きのマッピング) の両方にエントリーを作成します。配列は直接操作され、それ以外のものはスカラーとして操作されます (そして、配列の参照に挿入されます)。

リスト9. FETCH() 関数
# return the primary or secondary key, in that order (duplicate keys
# are not detected here)
sub FETCH
{
 my ($self, $key) = @_;
 exists $self->{PRIMARY}->{$key} &&
  return $self->{PRIMARY}->{$key};
 exists $self->{SECONDARY}->{$key} &&
  return $self->{SECONDARY}->{$key};
 return undef;
}

FETCH() 関数は、一次ハッシュまたは二次ハッシュからキーを取り出してきます。優先されるのは一次ハッシュです。(文1) && (文2) という論理ショートカットは、Perlの常套的な記法です。

リスト10.EXISTS() 関数
# return the primary or secondary key existence, in that order
# (duplicate keys are not detected here)
sub EXISTS
{
 my ($self, $key) = @_;
 return undef unless (exists $self->{PRIMARY} &&
                      exists $self->{SECONDARY});
 return (exists $self->{PRIMARY}->{$key} ||
         exists $self->{SECONDARY}->{$key});
}

EXISTS() 関数は、正方向のマッピング、逆向きのマッピングの順に、キーが存在するかチェックします。

リスト11.DELETE() 関数
# delete the primary or secondary key, in that order (duplicate keys
# are not detected here)
sub DELETE
{
 my ($self, $key) = @_;
 return undef unless (exists $self->{PRIMARY} &&
                      exists $self->{SECONDARY});
 # make sure to delete reverse associations as well
 if (exists $self->{PRIMARY}->{$key})
 {
  foreach (keys %{$self->{SECONDARY}})
  {
   delete $self->{SECONDARY}->{$_}->{$key};
   delete $self->{SECONDARY}->{$_}
    unless scalar keys %{$self->{SECONDARY}->{$_}};
  }
  return delete $self->{PRIMARY}->{$key};
 }
 if (exists $self->{SECONDARY}->{$key})
 {
  foreach (keys %{$self->{PRIMARY}})
  {
   delete $self->{PRIMARY}->{$_}->{$key};
   delete $self->{PRIMARY}->{$_}
    unless scalar keys %{$self->{PRIMARY}->{$_}};
  }
  return delete $self->{SECONDARY}->{$key};
 }
}

削除は、削除しようとしているキーを、逆向きマッピングでも削除する必要がありますので、少し複雑になります。a->bという関係付けを行う場合、b->aの (二次の) マッピングも削除し、マッピングの配列値が空なら、それも削除する必要があります。

リスト12.CLEAR() 関数
sub CLEAR
{
 my ($self, $key) = @_;
 %$self = ();                           # clear the whole hash
 return 1;
}

内部ハッシュをクリアするのは、非常に単純な話で、STORE() に自動蘇生機能 (auto-vivification) がありますので、このオブジェクトをそのまま使い続けることができます。

リスト13.FIRSTKEY() 関数、NEXTKEY() 関数
sub FIRSTKEY
{
 my ($self) = @_;
 return undef unless (exists $self->{PRIMARY} &&
                      exists $self->{SECONDARY});
 return each %{$self->{PRIMARY}};
}
sub NEXTKEY
{
 my ($self, $lastkey) = @_;
 return undef unless (exists $self->{PRIMARY} &&
                      exists $self->{SECONDARY});
 return each %{$self->{PRIMARY}};
}

巡回関数 (iterators) のFIRSTKEY()NEXTKEY() は、複雑に見えるかもしれませんが、実際は、すべての処理をPerlのeach() 関数にやらせています。

リスト14.secondary_keys() 関数
sub secondary_keys
{
 my ($self) = @_;
 return undef unless (exists $self->{PRIMARY} &&
                      exists $self->{SECONDARY});
 return keys %{$self->{SECONDARY}};
}

FIRSTKEY()NEXTKEY() での通常のkeys() による巡回は、一次キーだけを対象としますので、二次マッピング用にsecondary_keys() 関数を用意しています。

まとめ

残念ながら、ファイル・ハンドルのタイは複雑すぎるため、本稿では取り上げませんでした。ファイル・ハンドルのタイを利用すれば、魅力的なことを実現できるようになります。たとえば、ファイル・ハンドルの読み書きを行うことで、データベースへの直接的な読み書きを行ったり、ファイルを閉じたときに電子メールを送信するといったことが可能になります。

ディスク (ファイル) のデータベースをハッシュにタイする方法は、よく取り上げられているテーマです。perldoc DB_Fileのドキュメントを読んだり、Programming Perl Third Edition という書籍の関係する章を参考にしたり、いろいろなオンラインのチュートリアルを読んでみてください (そうした参考文献へのリンクをいくつか参考文献に示しておきます)。

とはいっても、今回解説した内容から、タイ変数が非常に便利であることがわかります。CPANには、変数のタイを扱っているモジュールがたくさんありますので、いろいろ調べてみるとよいでしょう。きっと、皆さんのニーズに合ったモジュールが見つかることと思います。


ダウンロード可能なリソース


関連トピック

  • 本稿の内容を確認するには、Perl 5.05以上が必要です。CPANといったほうが通りがよいのでしょうが、Comprehensive Perl Archive Networkには、Perlのソース・コードの他、いろいろなものがごまんと掲載されています。
  • また、CPANには、皆さんがほしいと思うようなありとあらゆるPerlモジュール が用意されています。
  • PerlおよびPerl関連の情報は、Perl.com でも紹介されています。
  • タイ変数についてもっと学びたいと思っている方は、Larry Wall、Tom Christiansen、Jon Orwant共著のProgramming Perl Third Edition (O'Reilly & Associates、2000) を読んでみてはいかがでしょうか。最近の5.005および5.6.0のPerlを扱っている最良の参考書です。第14で変数のタイを扱っており、素晴らしい 参考文献です。
  • モジュール:
  • developerWorks のLinuxゾーンには、他にもLinux開発者向けの参考文献が多数掲載されています。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=228122
ArticleTitle=洗練されたPerl: タイ変数 (Tied variables)
publish-date=01012003