Accidentally repeating a Moo build step
While working on a recent side-project1, I came across a weird bug that took me a while to untangle, and I thought I’d make a note about it before I forget.
The details of the issue are messy, and as usual, not needed to understand the core of the problem. In fact, a snippet like the one below is enough to reproduce it:
To provide some minimal background, Moo objects will call a method
called BUILD
right after they are constructed, and will do so for each
of their parent classes, starting from the top-most parent.
Moo also provides a way to extend methods by means of the before
,
after
, and around
keywords, which will execute some code before, after,
or instead of a given method.
In this case, the intended behaviour was that creating a Child
object
would run the parent BUILD
, and then execute some additional code (the one
specified with after
).
However, creating objects of these classes resulted in the following (the comments show what would be printed):
Parent->new;
# Build Parent
Child->new;
# Build Parent
# Build Parent
# After BUILD
The parent BUILD
stage runs twice (this also happens with around
and before
)
Wait… what?
The parent BUILD
triggered twice.
As it turned out, the problem was that the child class was extending a method that it wasn’t defining, and the correct way to obtain the behaviour I expected is something like this:
package Child {
use Moo;
extends 'Parent';
sub BUILD { say 'Build Child' };
}
Child->new;
# Build Parent
# Build Child
After thinking about this for a bit2, I realised that my initial code
could not possibly have been expected to work as intended: the Moo
documentation clearly states that BUILD
methods will be called from the
top-most parent class down to the last sub-class, in that order.
Since I was installing a modifier in a sub-class, Moo could not be expected
to know about it when executing the BUILD
steps of the parent classes (and
child classes shouldn’t really be able to mess with their parents’ setup).
But I think this sort of thing could at least come with a warning.
In fact, if you extend a method that does not exist (including a BUILD
method if it doesn’t exist in the inheritance tree), you get an exception:
The method ‘BUILD’ is not found in the inheritance hierarchy for class Foo
And if you extend any other method, or at least any other that is also not one of the special ones that Moo calls on its own, the original method does not get called multiple times:
package Parent {
use Moo;
sub foo { say 'Foo!' };
}
package Child {
use Moo;
extends 'Parent';
after foo => sub { say 'Bar!' };
}
Child->new->foo;
# Foo!
# Bar!
In the end, this (like many other of the more interesting bugs) is more the result of a series of unfortunate events, than anything else. Complex systems are complex, and their interactions are more so.
Still, I think a warning in this case might be warranted: I’d be pressed
to find a legitimate case in which someone extending a BUILD
method
without declaring one is not a mistake.
And I know it would have saved me a good couple of hours.