Monday, May 4, 2009

Moose beginners: clear, predicate, and triggers

One of the nice things about Moose is that it adds another state for your instance variables -- whether or not the variable is actually set. With standard Perl variables you can check whether the variable is defined or undefined and true or false. In Moose the state of being undefined is different from the state of being set. In order to take advantage of this additional state you need to use the 'clearer' and 'predicate' methods for your attribute:
   has 'my_var' => ( isa => 'Str|Undef', is => 'rw', 
          clearer => 'clear_my_var',
          predicate => 'has_my_var' );
Setting 'my_var' to 'undef' is different than doing 'clear_my_var'. If you set it to undefined:
   $my_obj->my_var(undef);
then the predicate 'has_my_var' will return true. If you check for truth in the usual way:
   if( $my_obj->my_var ) { ... }
false will be returned for both the case where the attribute has been set to undefined and has been cleared. So you have to to use the predicate method:
   if( $my_obj->has_my_var ) { ... }
The predicate method will return true if 'my_var' has been set to undefined, and false if 'my_var' has been cleared.
An important piece of related behavior is that a trigger on an attribute is called when you set it, whether or not you are setting it to undefined, but the trigger is not called when you do a clear. So if you have an object in which only one of two variables should have a value, you can create triggers for both of them and use clear to un-set the other variable.
   has 'my_var' => ( isa => 'Str', is => 'rw',
           clearer => 'clear_my_var',
           predicate => 'has_my_var',
           trigger => sub { shift->clear_my_other_var }
   );
   has 'my_other_var' => ( isa => 'Str', is => 'rw',
           clearer => 'clear_my_other_var,
           predicate => 'has_my_other_var',
           trigger => sub { shift->clear_my_var }
   );
If you tried to set 'my_var' to undef in the trigger, you would end up in an infinite recursion, since each attempt to set the other variable would cause the trigger in that variable to fire. Another issue is whether or not you should allow a particular attribute to be set to undefined. Some attributes may need an explicit undefined state, in which case you must set your isa to 'Str|Undef', but if you don't actually need an undefined state then you are better off not allowing it and using a predicate, in which case you must clear the variable to un-set it since setting it to undef will fail.

2 comments:

hobbified said...
This comment has been removed by the author.
hobbified said...

Just for the sake of completeness I'd like to point out that this is totally consistent with the rest of Perl. Variables on their own are either undefined or defined. But slots in some aggregate (an array or a hash) have three states -- they hold a variable that is defined, they hold a variable that is undefined, or they are unset (!exists). Usually we don't go to the trouble to distinguish between "defined $account{balance}" and "exists $account{balance}" but when we need to, we can. Moose extends this aggregate-like behavior to objects, with "clear" being like "delete", and predicates being like "exists".