Using Moo::Role
, I'm finding that circular imports are silently preventing the execution of the before
modifier of my method.
I have a Moo::Role
in MyRole.pm
:
package MyRole;
use Moo::Role;
use MyB;
requires 'the_method';
before the_method => sub { die 'This has been correctly executed'; };
1;
...a consumer in MyA.pm
:
package MyA;
use Moo;
with ( 'MyRole' );
sub the_method { die; }
1;
..and another in MyB.pm
:
package MyB;
use Moo;
with ( 'MyRole' );
sub the_method { die 'The code should have died before this point'; }
1;
When I run this script.pl
:
#!/usr/bin/env perl
package main;
use MyA;
use MyB;
MyB->new()->the_method();
...I get The code should have died before this point at MyB.pm line 4.
but would expect to see This has been correctly executed at MyRole.pm line 5
.
I think this problem is caused by the circular imports. It goes away if I switch the order of the use
statements in script.pl
or if I change the use MyB;
in MyRole.pm
to be a require
within the_method
.
Is this behaviour expected? If so, what is the best way to handle it where circular imports can't be avoided?
I can workaround the problem but it feels worryingly easy to inadvertently trigger (particularly since it causes before
functions, which often contain checking code, to be silently skipped).
(I'm using Moo version 2.003004. Obviously the use MyB;
in MyRole.pm
is superfluous here but only after I've simplified the code for this repro example.)
Circular imports can get rather tricky, but behave consistently. The crucial points are:
use Some::Module
behaves likeBEGIN { require Some::Module; Some::Module->import }
BEGIN
blocks are executed during parsing of the surrounding code.require
'd once. When it is required again, thatrequire
is ignored.Knowing that, we can combine your four files into a single file that includes the
require
d files in a BEGIN block.Let's start with your main file:
We can transform the
use
toBEGIN { require ... }
and include theMyA
contents. For clarity, I will ignore any->import
calls onMyA
andMyB
because they are not relevant in this case.The
with('MyRole')
also does arequire MyRole
, which we can make explicit:So let's expand that:
We can then expand the
use MyB
, also expanding MyB'swith('MyRole')
to arequire
:Within
MyB
we have arequire MyRole
, but that module has already been required. Therefore, this doesn't do anything. At that point during the execution,MyRole
only consists of this:So the role is empty. The
requires 'the_method'; before the_method => sub { ... }
has not yet been compiled at that point.As a consequence
MyB
composes an empty role, which does not affect thethe_method
.How can this be avoided? It is often helpful to avoid a
use
in these cases because that interrupts parsing, before the current module has been initialized. This leads to unintuitive behaviour.When the modules you
use
are just classes and do not affect how your source code is parsed (e.g. by importing subroutines), then you can often defer the require to run time. Not just the run time of the module where top-level code is executed, but to the run time of the main application. This means sticking yourrequire
into the subroutine that needs to use the imported class. Since arequire
still has some overhead even when the required module is already imported, you can guard the require likestate $require_once = require Some::Module
. That way, the require has no run-time overhead.In general: you can avoid many problems by doing as little initialization as possible in the top-level code of your modules. Prefer being lazy and deferring that initialization. On the other hand, this laziness can also make your system more dynamic and less predictable: it is difficult to tell what initialization has already happened.
More generally, think hard about your design. Why is this circular dependency needed? You should decide to either stick to a layered architecture where high-level code depends on low-level code, or use dependency inversion where low-level code depends on high-level interfaces. Mixing both will result in a horrible tangled mess (exhibit A: this question).
I do understand that some data models necessarily feature co-recursive classes. In that case it can be clearest to sort out the order manually by placing the interdependent classes in a single file.