Of course you can `requires` attributes!

During the patch -p2 hackathon, two seasoned Perl programmers told me that "you can't requires attributes". This sounded weird to me, as attributes are exposed as methods and besides, I'm doing that all the time in my current work project.

Let's look at the simplest possible example:

package MyRole;
use Moo::Role;
requires 'my_attr';

package MyClass;
use Moo;
with 'MyRole';
has my_attr => ( is => 'ro' );

And then run it:

`Can't apply MyRole to MyClass - missing my_attr at /home/book/perl5/lib/perl5/Role/Tiny.pm line 317.`.

Indeed, that looks like it doesn't work.

Let's consider what the code does for a moment:

  • requires checks if a subroutine my_attr exists in the MyClass namespace
  • has 'my_attr' adds the my_attr subroutine (an accessor) in the MyClass namespace

Why doesn't this work? First, we should keep in mind that our beloved Perl has a complex organisation of compile-time and runtime phases. Although Moo (and Moose) code is declarative, most of it is nonetheless executed during a runtime phase. Second, we can fix this without looking at the code of any of Moo, Moo::Role or Role::Tiny. Simply use our knowledge of Perl, and realize what is happening.

Calling with is asking the roles to check (among other things) if the methods they require actually exist in the calling class. But this line of code is executed before the has, which actually inserts my_attr in the class namespace. So my_attr doesn't exist in MyClass at the time with is called.

The fix is simple enough. Let's simply swap the with and has lines:

package MyRole;
use Moo::Role;
requires 'my_attr';

package MyClass;
use Moo;
has my_attr => ( is => 'ro' );
with 'MyRole';

Which compiles (and runs) just fine.

I think my with declarations are going to move to the bottom of my files as I design more of my code around roles.

4 Comments

It works in Moo, but there are some bugs with requires and attributes in Moose.

Demonstration...

use v5.14;

package AAA {
   use Moose::Role;
   requires 'b';
   has 'a' => (is => 'ro');
}
 
package BBB {
   use Moose::Role;
   requires 'a';
   has 'b' => (is => 'ro');
}
 
package CCC {
   use Moose;
   with qw( AAA BBB );
}

A cleaner way (in my opinion, yours may differ), is to use forward declarations. The promise a subroutine will be there, even if it doesn't exist at compile time. That means you can put your with statement wherever you like. I've encountered cases with nested roles where even a trailing with doesn't solve the problem.

    package MyRole {
        use Moo::Role;
        requires 'my_attr';
    }

    package MyClass {
        use Moo;
        with 'MyRole';
    
        sub my_attr;
        has my_attr => ( is => 'ro' );
    }

I noticed the issue and I trusted in developers desgin, so I supposed the right way was to put attributes in the MyRole package.

If an attribute needs some method in its builder or default sub, it can be added in the requires list. For example you can require the builder sub.

Leave a comment

About BooK

user-pic Pink.