Do you copy?
One of my guiding principles when writing code is "Mutable state is the enemy. Mutable state is always the enemy."
Now, arguably, a more accurate statement would be that it's shared mutable state that's the problem but (a) the less of it there is, the less of it can end up shared and (b) it makes 'enemy of the state' jokes much clunkier, and, y'know, terrible puns are important.
This basically means that my Moo classes have attributes that almost all follow one of these two patterns -
has passed_in_thing => (is => 'ro', required => 1);
has calculated_thing => (is => 'lazy', builder => sub { ... });
The first sort must be passed to new() and is read only from then on. The second sort can be passed to new(), but if it isn't then the first time you try and read it, the builder code will be called as a method to generate the value and then after that it's read only.
(Why use builder instead of default? Because the builder form installs the code as _build_attribute_name and so you can override and/or method modifier it much more easily.)
This is great. Except then you want an object that's almost exactly the same as the one you already have but with one or two attributes tweaked. Now, given Moose, you could construct something using reflection to do this ... but it turns out that it's not completely horrible to do in plain perl.
Well, ok, the idiomatic version that I drop in if I have a single class that needs it is moderately horrible -
sub but { ref($_[0])->new(%{$_[0]}, @_[1..$#_]) }
but, realistically, you probably want to (a) put it in a role somewhere (b) write it rather more like this -
sub but {
my ($self, @args) = @_;
ref($self)->new(%$self, @args);
}
which should make what's going on reasonably clear - ref() returns the class of your object (I'm comfortable with calling this as a class method blowing up since that's utterly nonsensical), and then we deref $self to get a shallow copy of the current attribute values, and append any arguments passed so they override the current values. Thus -
my $obj = Class->new(foo => 1, bar => 2);
my $obj_but = $obj->but(foo => 3);
will result in a $obj_but that has a bar value of 2 still but a foo value of 3.
Now, admittedly, this relies on your object being a hashref. However my Devel::Dwarn addiction dictates that all my objects are, indeed, hashref based so that isn't really a problem. However, the question that then springs to mind is - how do I exclude an attribute from being copied?
Simple - you make its init_arg different from the attribute's name. So -
has _dont_clone_me => (
is => 'lazy', init_arg => 'dont_clone_me',
builder => sub { ... }
);
will ensure that the attribute doesn't get cloned - but still allows you to pass it as a constructor arg for testing (or, presumably, other nefarious purposes, but you probably didn't want to do that). Note that this does then mean that the builder method will be _build__dont_clone_me, since it's the attribute name that's used to generate it - it's perfectly legal to have both a foo and an _foo attribute so we can't avoid that.
If you want a private attribute that's passable from the constructor and still gets copied, you can't just do -
has _no_reading_this => (
is => 'ro', required => 1, init_arg => 'no_reading_this'
);
because now the init_arg doesn't match and it won't get copied. However, there's nothing wrong with instead doing -
has no_reading_this => (
is => 'ro', required => 1, reader => '_no_reading_this'
);
at which point the reader method is private but the attribute initialises and copies the way you'd expect it to.
The last thing I'll tend to do is to add methods that are additive rather than replacing something entirely -
sub with_options {
my ($self, %options) = @_;
$self->but(options => { %{$self->options}, %options });
}
and similarly for arrayref attributes
sub with_search_paths {
my ($self, @paths) = @_;
$self->but(search_paths => [ @paths, @{$self->search_paths} ]);
}
This process could, I guess, be sugared into an extension, but so far I'm finding that there are enough tiny differences (prepending versus appending to an arrayref attribute, for example) that I prefer to consider it a pattern.
Here's to remaining, now and forever more, an enemy of the (mutable) state.
-- mst, out.