Friday, August 28, 2009

Moose Triggers

Moose lets you provide a callback that is invoked when an attribute is changed.

Here is an example of "height" and "width" attributes automatically updating an "area" attribute when either of them is modified:

has [qw(height width)] => (
    isa     => "Num",
    is      => "rw",
    trigger => sub {
        my $self = shift;

has area => (
    isa        => "Num",
    is         => "ro",
    lazy_build => 1,

sub _build_area {
    my $self = shift;
    return $self->height * $self->width;

The Problem

Unfortunately the implementation is sort of weird. To quote the documentation:

NOTE: Triggers will only fire when you assign to the attribute, either in the constructor, or using the writer. Default and built values will not cause the trigger to be fired.

This is a side effect of the original implementation, one that we can no longer change due to compatibility concerns.

Keeping complicated mutable state synchronized is difficult enough as it is, doing that on top of confusing semantics makes it worse. There are many of subtle bugs that you could accidentally introduce, which become very hard to track down.

In #moose's opinion (and mine too, obviously), anything but trivial triggers that just invoke a clearer should be considered code smell.


If you find yourself reaching for triggers there are a number of other things you can do.

Usually the best way is to avoid mutable state altogether. This has a number of other advantages (see older posts: 1, 2, 3). If you can eliminate mutable state you won't need triggers at all. If you merely reduce mutable state you can still minimize the number and complexity of triggers.

The general approach is to take all of those co-dependent attributes and move them into a separate class. To update the data an instance of that class is constructed from scratch, replacing the old one.

Another approach is to just remove the notion of an attribute from your class's API. Make the attributes that store the mutable data completely private (don't forget to set their init_arg to undef), and in their place provide a method, preferably one with a clear verb in its name, to modify them in a more structured manner.

Instead of implicitly connecting the attributes using callbacks they are all mutated together in one well defined point in the implementation.

The method can then be used in BUILD allowing you to remove any other code paths that set the attributes, so that you only have one test vector to worry about.

The code that Moose generates for you should be helpful, don't let it get in the way of producing clean, robust and maintainable code.

No comments: