題名¶
Moose::Cookbook::Basics::Recipe3 - 遅延評価を行うBinaryTreeの例
概要¶
package BinaryTree;
use Moose;
has 'node' => ( is => 'rw', isa => 'Any' );
has 'parent' => (
is => 'rw',
isa => 'BinaryTree',
predicate => 'has_parent',
weak_ref => 1,
);
has 'left' => (
is => 'rw',
isa => 'BinaryTree',
predicate => 'has_left',
lazy => 1,
default => sub { BinaryTree->new( parent => $_[0] ) },
trigger => \&_set_parent_for_child
);
has 'right' => (
is => 'rw',
isa => 'BinaryTree',
predicate => 'has_right',
lazy => 1,
default => sub { BinaryTree->new( parent => $_[0] ) },
trigger => \&_set_parent_for_child
);
sub _set_parent_for_child {
my ( $self, $child ) = @_;
confess "You cannot insert a tree which already has a parent"
if $child->has_parent;
$child->parent($self);
}
本文¶
このレシピでは、高度なアトリビュートの機能をいろいろ使って、複雑で強力な振る舞いを作る方法を説明します。とりわけここではpredicate
やlazy
、trigger
といった新しいアトリビュートのオプションを多数紹介していきます。
例題のクラスは、古典的なバイナリツリーです。ノードはそれぞれがBinaryTree
のインスタンスで、任意の値を入れられるnode
というアトリビュートと、子のツリーを参照しているright
アトリビュートとleft
アトリビュート、それからparent
というアトリビュートがあります。
node
アトリビュートから見ていきましょう。
has 'node' => ( is => 'rw', isa => 'Any' );
Mooseはこのアトリビュートに読み書き可能なアクセサを生成します。型制約はAny
なので、文字通り何でも入れられます。
isa
オプションは外してしまってもよかったのですが、ここではコンピュータのためではなく、ほかのプログラマのために入れておきました。
続いてparent
アトリビュートに移りましょう。
has 'parent' => (
is => 'rw',
isa => 'BinaryTree',
predicate => 'has_parent',
weak_ref => 1,
);
こちらも読み書き可能なアクセサがありますが、今度はisa
オプションによってこのアトリビュートはかならずBinaryTree
のインスタンスでなければならないと指定されています。2番目のレシピで見た通り、Mooseベースのクラスを作ると、かならず対応するクラスの型制約も用意されます。
predicate
は新しいオプションで、そのアトリビュートが初期化済みかどうかをチェックできるメソッドを生成するものです(ここでは、メソッド名はhas_parent
となります)。
そして、このアトリビュート最後のオプションであるweak_ref
ですが、parent
は循環参照しているので(parent
ツリーのleft
アトリビュートかright
アトリビュートにはすでにこのオブジェクトへの参照があるはずです)、確実にウィークリファレンスにしてメモリリークを避けたいところです。weak_ref
を真にすると、アクセサ関数が変化して、リファレンスを入れたらウィークリファレンスにしてくれるようになります。
最後はleft
アトリビュートとright
アトリビュートです。この2つは名前を除けば本質的には同じものですので、ここではleft
だけ見ることにします。
has 'left' => (
is => 'rw',
isa => 'BinaryTree',
predicate => 'has_left',
lazy => 1,
default => sub { BinaryTree->new( parent => $_[0] ) },
trigger => \&_set_parent_for_child
);
lazy
、default
、trigger
という新しいオプションが3つありますが、lazy
オプションとdefault
オプションはリンクしています。実は、lazy
アトリビュートが使えるのは、default
(あるいはあとで取り上げるbuilder
)があるときだけなのです。デフォルトを用意しないでアトリビュートを遅延評価しようとすると、クラスの生成に失敗して例外が発生します。(2)
2番目のレシピでは、BankAccountクラスのbalance
アトリビュートには0
というデフォルト値が用意されていました。このようにデフォルト値がリファレンスでない場合は「値」がコピーされるのですが、デフォルト値がリファレンスの場合は、ディープクローニングではなく、単にリファレンスがコピーされます。そのため、単純にデフォルトに素のリファレンスを指定すると、最初に生成されたリファレンスがそのままそのアトリビュートを持つすべてのオブジェクトに使い回されてしまいます。
この問題の回避策は、無名サブルーチンを使うことです。無名サブルーチンを使うと、デフォルトが呼ばれるたびに新しいリファレンスが生成されます。
has 'foo' => ( is => 'rw', default => sub { [] } );
もっとも、実際には、Mooseではデフォルトにサブルーチン以外のリファレンスを使うことはできないようになっています。
# will fail
has 'foo' => ( is => 'rw', default => [] );
これはエラーになりますのでしないでください。
お気づきの通り、ここではデフォルトサブルーチンの中で$_[0]
を使っています。デフォルトのサブルーチンは、実行時にはそのオブジェクトのメソッドとして呼ばれるためです。
この例では、デフォルトとして、現在のツリーを親に持つ新しいBinaryTree
オブジェクトを作っています。
通常、デフォルト値はオブジェクトがインスタンス化されるとすぐに評価されます。ところが、このBinaryTree
クラスの場合、それは大問題になりかねません! 最初のオブジェクトを作ったとき、すぐにleft
アトリビュートとright
アトリビュートの初期化が行われると、そこでもまた新しいBinaryTree
ができ、それがまた自身のleft
、right
スロットを初期化しようとして、大惨事になってしまいます!
left
アトリビュートとright
アトリビュートをlazy
にしておくとこの問題は回避できます。アトリビュートの値を読み込むとき、すでに値が存在していればデフォルトはいっさい実行されなくなります。
最後にもうひとつ追加しておきたい振る舞いがあります。自動的に生成されるright
やleft
のアクセサは期待通りの働きをしてくれるとはいえません。left
ないしright
アトリビュートに値をセットしたら、忘れずにそのツリーの親も更新しておきたいところです。
ここで自前のアクセサを用意してもよいのですが、それではMooseを使っている意味はありません。ここではそのかわりにtrigger
を使います。trigger
にサブルーチンリファレンスをセットすると、アトリビュートに値が書き込まれたときはかならずそのサブルーチンがメソッドとして呼ばれるようになります。このメソッド呼び出しは、オブジェクトが生成されるときでも、あとからアトリビュートのアクセサメソッドに新しいオブジェクトを渡すときでも起こりますが、default
やbuilder
経由で値が書き込まれた場合には起こりません。
sub _set_parent_for_child {
my ( $self, $child ) = @_;
confess "You cannot insert a tree which already has a parent"
if $child->has_parent;
$child->parent($self);
}
このトリガでは2つのことをしています。まず、新しい子ノードがすでに親を持っていないかを確認しています(これは例を簡単にするためです。もっと賢くしたいのであれば、古い親ツリーからその子を削除して新しいツリーに追加するところでしょう)。
子ノードに親がない場合は現在のツリーに追加して、parent
アトリビュートには確実に正しい値が設定されるようにします。
ほかのレシピの場合と同じく、BinaryTreeもほかのPerl 5のクラスと同じように使えます。t/000_recipes/moose_cookbook_basics_recipe3.tにはもっと詳しい使用例があります。
まとめ¶
このレシピではMooseの高度な機能をいくつか紹介しました。このレシピがほかのところでもみなさんのコードをシンプルにするお役に立てば幸いです。
1¶
-
ウィークリファレンスはトリッキーなものですから、控えめに、(循環参照がある場合のように)適切な理由があるときのみ使うようにしてください。気をつけないと、アトリビュートの値が「不思議な」消え方をすることがあります(これは、Perlの参照カウント式ガベージコレクタが走ってウィークリファレンスにしておいた値を削除してしまうためです)。
要するに、なにをやっているかわからないのであれば使わないように、ということです。:)
- (2)
-
2番目のレシピで紹介したように、お望みであれば
lazy
オプションなしでdefault
オプションを使うことは「できます」。また、
default
のかわりにbuilder
を使うこともできます。詳しくはMoose::Cookbook::Basics::Recipe8をご覧ください。
作者¶
Stevan Little <stevan@iinteractive.com>
Dave Rolsky <autarch@urth.org>
コピーライト & ライセンス¶
Copyright 2006-2009 by Infinity Interactive, Inc.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.