From: Stevan Little Date: Mon, 27 Mar 2006 18:36:20 +0000 (+0000) Subject: more-cookin X-Git-Tag: 0_05~56 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=8597d9504c6522c26e660bc6070b8a27471a42f4;p=gitmo%2FMoose.git more-cookin --- diff --git a/lib/Moose/Cookbook/Recipe3.pod b/lib/Moose/Cookbook/Recipe3.pod index 3625a30..d2bfcad 100644 --- a/lib/Moose/Cookbook/Recipe3.pod +++ b/lib/Moose/Cookbook/Recipe3.pod @@ -12,24 +12,26 @@ Moose::Cookbook::Recipe3 - A binary tree use warnings; use Moose; + has 'node' => (is => 'rw', isa => 'Any'); + has 'parent' => ( - is => 'rw', - isa => 'BinaryTree', + is => 'rw', + isa => 'BinaryTree', predicate => 'has_parent', - weak_ref => 1, + weak_ref => 1, ); has 'left' => ( - is => 'rw', - isa => 'BinaryTree', + is => 'rw', + isa => 'BinaryTree', predicate => 'has_left', lazy => 1, default => sub { BinaryTree->new(parent => $_[0]) }, ); has 'right' => ( - is => 'rw', - isa => 'BinaryTree', + is => 'rw', + isa => 'BinaryTree', predicate => 'has_right', lazy => 1, default => sub { BinaryTree->new(parent => $_[0]) }, @@ -42,6 +44,195 @@ Moose::Cookbook::Recipe3 - A binary tree =head1 DESCRIPTION +In this recipe we take a closer look at attributes, and see how +some of their more advanced features can be used to create fairly +complex behaviors. + +The class in this recipe is a classic binary tree, each node in the +tree is represented by an instance of the B class. Each +instance has a C slot to hold an abitrary value, a C +slot to hold the right node, a C slot to hold the left node, +and finally a C slot to hold a reference back up the tree. + +Now, lets start with the code, our first attribute is the C +slot, defined as such: + + has 'node' => (is => 'rw', isa => 'Any'); + +If you recall from the previous recipies, this slow will have a +read/write accessor generated for it, and has a type constraint on it. +The new item here is the type constraint of C. In the type +constraint heirarchy in L, the C +constraint is the "root" of the hierarchy. It means exactly what it +says, it allows anything to pass. Now, you could just as easily of left +the C out, and left the C slot unconstrainted and gotten the +same behavior. But here, we are really including the type costraint +for the benefit of other programmers, not the computer. It makes +clear my intent that the C can be of any type, and that the +class is a polymorphic container. Next, lets move onto the C +slot. + + has 'parent' => ( + is => 'rw', + isa => 'BinaryTree', + predicate => 'has_parent', + weak_ref => 1, + ); + +As you already know from reading the previous recipes, this code +tells you that C gets a read/write accessor, is constrainted +to only accept instances of B. You will of course remember +from the second recipe that the C type constraint is +automatically created for us by Moose. + +The next attribute option is new though, the C option. +This option creates a method, which can be used to check to see if +a given slot (in this case C) has a defined value in it. In +this case it will create a method called C. Quite simple, +and also quite handy too. + +This brings us to our last attribute, and also a new one. Since the +C is a circular reference (the tree in C should +already have a reference in either it's C or C nodes), +we want to make sure that it is also a weakened reference to avoid +memory leaks. The C attribute option will do just that, +C simply takes a boolean value (C<1> or C<0>) and it will +then add the extra capability to the accessor function to weaken +the reference of any value stored in the C slot (1). + +Now, onto the C and C attributes. They are essentially +the same things, only with different names, so I will just describe +one here. + + has 'left' => ( + is => 'rw', + isa => 'BinaryTree', + predicate => 'has_left', + lazy => 1, + default => sub { BinaryTree->new(parent => $_[0]) }, + ); + +You already know what the C, C and C<>predicate> options +do, but now we have two more new options. These two options are +actually linked together, in fact, you cannot use the C +option unless you have set the C option. The class +creation will fail with an exception (2). + +Before I go into detail about how C works, let me first +explain how C works, and in particular why it is wrapped +in a CODE ref. + +In the second recipe the B's C slot had a +default value of C<0>. Since Perl will copy strings and numbers +by value, this was all we had to say. But for any other item +(ARRAY ref, HASH ref, object instance, etc) Perl will copy by +reference. This means that if I were to do this: + + has 'foo' => (is => 'rw', default => []); + +Every single instance of that class would get a pointer to the +same ARRAY ref in their C slot. This is almost certainly +B the behavior you intended. So, the solution is to wrap +these defaults into an anon-sub, like so: + + has 'foo' => (is => 'rw', default => sub { [] }); + +This assures that each instance of this class will get it's own +ARRAY ref in the C slot. + +One other feature of the sub ref version of the C option +is that when the subroutine is executed (to get back the expected +default value), we also pass in the instance where the slot will +be stored. This added feature can come in quite handy at times, as +is illustrated above, with this code: + + default => sub { BinaryTree->new(parent => $_[0]) }, + +The default value being generated is a new C instance +for the C (or C) slot. Here we set up the parental +relationship by passing the current instance to the constructor. + +Now, before we go on to the C option, I want you to think +for a moment. When an instance of this class is created, and the +slots are being initialized, the "normal" behavior would be for +the C and C slots to be populated with a new instance +of B. In creating that instance of the C or +C slots, we would need to create new instances to populate +the C and C slots of I instances. This would +continue in an I until you had +exhausted all available memory on your machine. + +This is, of course, not good :) + +Which brings us to the C attribute option. The C option +does just what it says. It lazily initializes the slot within the +instance. This means that it waits till the I last possible +moment to populate the slot. This means that if you, the user, write +to the slot, everything happens as normal and what you pass in is stored. +However, if you I the slot, then at that I moment (and no +sooner), the slot will be populated with the value of the C +option. + +This option is what allows the B class to instantiate +objects without fear of the I +I mentioned earlier. + +So, we have descibed a quite complex set of behaviors here, and not +one method has needed to be written. But wait, we can't get away that +easily. The autogenerated C and C accessors are not +completely correct. They will not install the parental relationships +that we need. We could write our own accessors, but that would require +us to implement all those features we got automatically (the type +constraint, the lazy initialization, etc). So instead we use the +method modifiers again. + + before 'right', 'left' => sub { + my ($self, $tree) = @_; + $tree->parent($self) if defined $tree; + }; + +This is a C modifier, just like we saw in the second recipe, +but with two slight differences. First, we are applying this to more +than one method at a time. Since both the C and C methods +need the same feature, it makes sense. The second difference is that +we are not wrapping an inherited method anymore, but instead a method +of our own local class. Wrapping local methods is no different, the +only requirement is that the wrappee be created before the wrapper +(after all, you cannot wrap something which doesn't exist right?). + +Now, as with all the other recipes, you can go about using +B like any other Perl 5 class. A more detailed example of +usage can be found in F. + +=head1 CONCLUSION + +This recipe introduced you to some of the more advanced behavioral +possibilities of Moose's attribute mechanism. I hope that it has +opened your mind to the powerful possibilities of Moose. In the next +recipe we explore how we can create custom subtypes and take +advantage of the plethora of useful modules out on CPAN with Moose. + +=head1 FOOTNOTES + +=over 4 + +=item (1) + +Weak references are tricky things, and should be used sparingly +and appropriately (such as in the case of circular refs). If you +are not careful, you will have slot values disappear "mysteriously" +because perls reference counting garbage collector has gone and +removed the item you are weak-referencing. + +In short, don't use them unless you know what you are doing :) + +=item (2) + +You I use the C option without the C option if +you like, as we showed in the second recipe. + +=back + =head1 AUTHOR Stevan Little Estevan@iinteractive.comE diff --git a/t/003_basic.t b/t/003_basic.t index c558dc4..cdff83a 100644 --- a/t/003_basic.t +++ b/t/003_basic.t @@ -3,7 +3,7 @@ use strict; use warnings; -use Test::More tests => 25; +use Test::More tests => 34; use Test::Exception; use Scalar::Util 'isweak'; @@ -18,6 +18,8 @@ BEGIN { use warnings; use Moose; + has 'node' => (is => 'rw', isa => 'Any'); + has 'parent' => ( is => 'rw', isa => 'BinaryTree', @@ -47,9 +49,11 @@ BEGIN { }; } -my $root = BinaryTree->new(); +my $root = BinaryTree->new(node => 'root'); isa_ok($root, 'BinaryTree'); +is($root->node, 'root', '... got the right node value'); + ok(!$root->has_left, '... no left node yet'); ok(!$root->has_right, '... no right node yet'); @@ -71,11 +75,31 @@ ok(isweak($left->{parent}), '... parent is a weakened ref'); ok(!$left->has_left, '... $left no left node yet'); ok(!$left->has_right, '... $left no right node yet'); +is($left->node, undef, '... left has got no node value'); + +lives_ok { + $left->node('left') +} '... assign to lefts node'; + +is($left->node, 'left', '... left now has a node value'); + # make a right node +ok(!$root->has_right, '... still no right node yet'); + +is($root->right->node, undef, '... right has got no node value'); + +ok($root->has_right, '... now we have a right node'); + my $right = $root->right; isa_ok($right, 'BinaryTree'); +lives_ok { + $right->node('right') +} '... assign to rights node'; + +is($right->node, 'right', '... left now has a node value'); + is($root->right, $right, '... got the same node (and it is $right)'); ok($root->has_right, '... we have a right node now');