Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

Perl で楽々アクセサ作成について考えてみた - その 4

思いのほか多くの反応をいただいて、予想以上に長期化した本シリーズですが、ここにきてなんと小飼弾氏(以下 dan 氏)の「404 Blog Not Found:Never overload &UNIVERSAL::AUTOLOAD」からのトラックバックをいただき、あの dan 氏にコードでもってご教示いただけたなんて、もうこれはほんとまさにインターネッツの醍醐味! 感謝・感激の極みですよ!

さて、ご指摘の 2 点につき、思ったことをば以下に述べます。まずは、本シリーズその 1 におけるコメント欄での hio さんと僕とのやりとりについて。

感心している場合じゃないって!

404 Blog Not Found:Never overload &UNIVERSAL::AUTOLOAD

これはなんというか、ありていにいえば「ネタにマジレス」の類に属するのではなかろうかと思います。

dan 氏は &UNIVERSAL::AUTOLOAD を考えもなく定義することの危険性について説明し、同じことをしたいなら継承を使えばいいだけの話であると述べています。まったくもって正当な指摘であり、なんらの反論の余地がありません。というか、その危険性について改めて再確認するとともに、ラクダ本の関連ページを読み返すきっかけともなり、大変にありがたいことです。

しかし、本シリーズその 1 におけるコメント欄にて、Yappo さんへの返答として Class::AutoAccess を挙げ、また、まかまかさんによるコードに感激を受けていることからもわかる通り、話の流れとしては、ここではご指摘の継承による方法が考察されていたのです。

そこに、hio さんが &UNIVERSAL::AUTOLOAD を定義する方法を提示した。その回答には、$obj->foo->bar みたいなアクセサを作成したいという問題設定に対して、「答えを出すというだけなら、そんな面倒なことしなくてもこうすれば一発じゃーん」的、いわば斜めから hack 的ユーモアで以て答えるという、当然実用という観点からは問題ありまくりだけど、なんというかある種のエンターテインメント性をともなう鮮やかさがあり、僕はそのことに対して感心したのだし、hio さんもそういう意味であのようなコードを書かれたのだと思います。要するに、ネタだろう、ということです。

2 点目。「実はこの辺の話題はラクダ本にもきちんと出てきます。車輪や毒を再発明する前にきちんと目をとおしておきましょう」というご指摘について。『ラクダ本第 3 版 vol.1』の 395 ページ以降に、AUTOLOAD とクロージャによるアクセサの作成についての話題が述べられています。もちろん、先の記事を書いた際、参照しております。また、先に挙げた Class::AutoAccess はまさにそこに書かれているのとそう変わらない実装です(ラクダ本に書かれてる方法のほうが、ずっとスマートだとは思いますが)。

その 2」で示し、またそれを手直しして CPAN にあげた Class::AutoAccess::Deep は、そもそもの僕のプログラミングスキル不足を反映して、アクセサが呼び出されるたびに AUTOLOAD が呼び出されることになり、大変に非効率的なものになっています。ラクダ本の当該箇所を参照していながら、なぜそのような事態に陥っているのか。もちろん僕の能力不足が原因なのですが、少なくとも僕が意図していたのは、dan 氏の示した解決策とはちょっと違うのです。

TODO でも述べている通り、現状ではアクセサを呼び出すたびに AUTOLOAD することになるってな非効率的な実装になっているので、一度呼び出したアクセサをなんらかの方法で保存して、再利用できるようにしたい。そのためには、たとえば以下の方法が考えられる。

  1. 各 ハッシュリファレンスは Class::AutoAccess::Deep の再帰構造となるのだが、それぞれのすでに生成されたインスタンスに、どうにかして一度呼び出されたアクセサメソッドをぶっこむ
  2. ハッシュリファレンスの階層をたどって Class::AutoAccess::Deep オブジェクトを連続的に作成するときに、ISA をいじってたとえば Class::AutoAccess::Deep::SandBox::1, Class::AutoAccess::Deep::SandBox::2 ... というような感じで、一意のパッケージ名をでっちあげ、そのパッケージ内に一度呼ばれたアクセサをぶっこむ

antipop2.0: Perl で楽々アクセサ作成について考えてみた - その 3

一見、「そんな面倒なことしなくても、ラクダ本通りにやれば解決じゃーん」と思われる内容です。僕も最初はそう思いました。しかし、それではダメなところ、また、きわめて主観的なものいいになりますがちょっとキモイと僕には思われたところがあるわけです。理由は以下の通り。

  1. Class::AutoAccessor::Deep は、元はといえば Class::Accessor や Class::AutoAccess に着想を得たものなので、それらにならい、フィールドへの無制限なアクセスを許さないようにした。よって、$AUTOLOAD に含まれるメソッド名をチェックして、未定義のフィールドへのアクセスに対してエラーを返さなければならない
  2. ネストされたハッシュリファレンスを階層をたどって Class::AutoAccess::Deep オブジェクトとして bless するのはまぁいいとして、AUTOLOAD 済メソッドを単にラクダ本のようにして、あるいは dan 氏が示したような方法で保存することはできない。そのような方法では、未定義なメソッドへのアクセスを許さないという、1 点目の要求を満たさない

1 点目については、未定義だろうがなんだろうがアクセス可にしても問題ないといえばないとも思うのですが、それはまぁ他のモジュールとの均衡を図らねばなぁという配慮も働いて、そういう仕様にしているわけです。そしてその仕様については、ドキュメントでも述べています(英語がおかしいというツッコミは勘弁してください……)。。

2 点目については、知識不足により言葉で以て説明するのが難しいので、コードで説明したいと思います

#!/usr/bin/env perl

use strict;
use warnings;

use Test::More tests => 2;
use Test::Exception;

package Class::Accessor::Nested;
use Carp;

sub AUTOLOAD {
    my $this = shift;
    my $name = our $AUTOLOAD;
    $name =~ s/.*:://o;
    $name eq 'DESTROY' and return;
    ref($this) && $name or croak "Undefined subroutine $name called";

    # Class::AutoAccess::Deep では、未定義のフィールドへのアクセスを許さない
    # 以下 1 行、Class::AutoAccess::Deep に合わせて dan 氏のコードに追加した
    croak "Field $name does not exists" unless exists $this->{$name};

    {
        no strict 'refs';
        *$name = sub {
            # warn "$name predefined";
            my $self = shift;
            @_ and $self->{$name} = shift;
            ref $self->{$name} eq 'HASH' and bless $self->{$name}, ref($self);
            $self->{$name};
        };
    }
    # warn "$name first called";
    @_ and $this->{$name} = shift;
    ref $this->{$name} eq 'HASH' and bless $this->{$name}, ref($this);
    $this->{$name};
}

package MyClass;

use base qw(Class::Accessor::Nested);

sub new {
    my $class = shift;
    my $data  = shift || {};
    bless $data, $class;
}

package main;

my $data = {
    foo => 'hoge',
    bar => {
        baz => 'fuga',
    },
};

my $obj = MyClass->new($data);

# この時点では、未定義のフィールドに対するアクセスが croak されるので
# テストが通る
dies_ok {$obj->baz};

$obj->bar->baz;

# 先に通ったテストが、ここでは通らない
dies_ok {$obj->baz};

上記コードの下部に示したコメントを参照していただきたいのですが、$obj->{baz} なんてフィールドは存在しないので、先に述べた仕様に沿えば $obj->baz としてアクセスすることは常に croak されるべきなのに、アクセサを保存する処理において、Class::Accessor::Nested オブジェクトがデータ構造のどの階層にあるかが考慮されていないため、このような挙動になるわけです。

これはまぁ、いってみればほんとどうでもいいっちゃぁどうでもいいことなのかもしれませんし、このことについてどう思うか訊いてみたある方も「別にそんなのどうでもいいじゃん」といってはいました。実際、どうでもいいことだと思います。ただ、それがどうでもいいことかどうかはおくとして、先に述べた仕様に反することには間違いないので、ない知恵をふりしぼって迂遠なことをあれこれと考えていたわけです。ですから、dan 氏のご指摘を拝読して大変ためになったのですが、僕の問題を解決するものとはちょっと違うのかなぁと思ったわけです。

Kentaro the Hetare Perler