원클릭으로
perl-moo
Moo Perl object system - roles, attributes, inheritance patterns, and best practices
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Moo Perl object system - roles, attributes, inheritance patterns, and best practices
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Use when managing Git-native kanban tasks or shared helper refs with the karr CLI in agent workflows.
Use when managing karr-foundation — periodic agent execution across multiple karr boards, drain loops, and auto-block logic.
Getty's house rules for all Perl code — module loading, Moose patterns, cpanfile versioning for Getty-authored CPAN distributions, stylistic choices that differ from defaults. Load on any Perl edit in a Getty project.
File::ShareDir — packaging and accessing non-code data files (templates, schemas, configs, skills) in Perl distributions.
SOC 직업 분류 기준
| name | Perl Moo |
| description | Moo Perl object system - roles, attributes, inheritance patterns, and best practices |
| trigger | when working with Moo, Moo::Role, Perl OO, or perl-moo |
| category | language |
Use inheritance sparingly (stable "is-a" contracts), roles heavily (horizontal reuse). When in doubt: role, not subclass.
extends + Attribute Overridepackage App::Base;
use Moo;
has prefix => (is => 'ro', default => sub { 'Hello' });
sub greet { $_[0]->prefix . ", " . $_[0]->name }
package App::Friendly;
use Moo;
extends 'App::Base';
has '+prefix' => (default => sub { 'Hi' }); # override via +attr
Rules: Multiple extends calls REPLACE (don't add). Reference defaults always as coderefs (sub { [] }, never []).
requirespackage App::Role::UppercaseName;
use Moo::Role;
requires 'name'; # contract: consumer must have name()
sub uppercase_name { uc $_[0]->name }
package App::User;
use Moo;
extends 'App::Base';
with 'App::Role::UppercaseName'; # composed; missing 'name' → loud failure
Rules: requires fails at composition time, not runtime. Imports inside a role land as methods on the consumer unless cleaned up – everything loaded before use Moo::Role is auto-cleaned; everything after is composed.
package App::Role::HasId; use Moo::Role; has id => (is => 'ro', required => 1);
package App::Role::CanDescribe;
use Moo::Role; requires 'id';
sub describe { "id=" . $_[0]->id }
package App::Thing;
use Moo;
with 'App::Role::HasId', 'App::Role::CanDescribe';
Use when there's no meaningful "is-a" relationship. Prefer over deep hierarchies.
package My::Mooish;
use Import::Into;
sub import {
my $target = caller;
strict->import::into($target);
warnings->import::into($target);
Moo->import::into($target);
namespace::clean->import::into($target); # after Moo, cleans stray imports
}
package App::Thing;
use My::Mooish;
has x => (is => 'ro', default => sub { 1 });
Rules: Order matters: imports → use Moo → namespace::clean. Use namespace::autoclean ≥ 0.16 only (older versions inflate Moo classes to Moose). Use strictures v2 with Moo 2.
handlespackage App::UsesCounter;
use Moo;
has counter => (
is => 'ro',
required => 1,
handles => 'App::Role::CounterAPI', # role defines the interface (inc/reset/value)
);
Three forms: handles => 'RoleName' (interface from role), handles => [qw(inc reset)] (list), handles => { add => 'push' } (rename). Does not trigger isa/coerce/trigger on the delegate itself.
Sub::HandlesViapackage Kitchen;
use Moo;
use Sub::HandlesVia;
use Types::Standard qw(ArrayRef Str);
has food => (
is => 'ro',
isa => ArrayRef[Str],
handles_via => 'Array',
default => sub { [] },
handles => { add => 'push', find => 'grep' },
);
Use Sub::HandlesVia over MooX::HandlesVia – the latter documents that triggers/coercions don't fire on delegated mutations. Load Sub::HandlesVia after use Moo.
before calc => sub { die "x<0" if $_[1] < 0 }; # validate, can't change return
around calc => sub {
my ($orig, $self, $x) = @_;
return $self->$orig($x) + 1; # can change return value
};
after calc => sub { ... }; # side-effects, logging
Rules: before/after cannot alter return value; around can. Always forward @_ correctly in around. Multiple modifiers from multiple roles stack – order is composition-order-sensitive.
has name => (is => 'ro', required => 1);
has tags => (is => 'ro', default => sub { [] }); # ALWAYS coderef for refs
has id => (is => 'lazy'); # built on first access
sub _build_id { "id:" . $_[0]->name }
has status => (
is => 'rw',
trigger => 1, # calls _trigger_status on set
);
sub _trigger_status { die "bad" unless $_[1] =~ /\A(new|ok)\z/ }
has _secret => (is => 'ro', init_arg => 'secret'); # constructor param alias
trigger fires on new() and set, NOT on default/builder. Old value is NOT passed (unlike Moose). is => 'lazy' = lazy reader, runs builder on first access.
around BUILDARGS => sub {
my ($orig, $class, @args) = @_;
return { source => $args[0] } if @args == 1 && !ref $args[0]; # normalize
$class->$orig(@args);
};
sub FOREIGNBUILDARGS { # maps args to non-Moo parent's constructor
my ($class, $args) = @_;
return ($args->{source});
}
sub BUILD { # runs AFTER all attributes are set; parent→child order
my ($self, $args) = @_;
die "invalid" unless length $args->{source};
}
# DEMOLISH: child→parent order. Never override DESTROY directly.
Do NOT call SUPER::BUILD manually – Moo handles the chain.
package StrictThing;
use Moo;
use MooX::StrictConstructor;
has size => (is => 'rw');
StrictThing->new(size => 5, colour => 'blue'); # dies: unknown attribute 'colour'
Caveat: Interacts with namespace::clean (can sweep new). If needed, protect via -except => ['new'] or adjust import order per the module docs.
# Single with → conflict → dies:
# with 'RoleA', 'RoleB'; # both define foo() → fatal
# Sequential with → first wins:
with 'RoleA'; # foo() from RoleA is now in the class
with 'RoleB'; # foo() already exists → RoleA wins silently
"Class wins": if the class defines foo() itself, neither role's version is used. For complex conflict strategies: refactor roles to avoid the overlap.
package Counter;
use Moo::Role;
use MooX::Role::Parameterized;
parameter name => (is => 'ro', required => 1);
role {
my ($p, $mop) = @_;
my $n = $p->name;
$mop->has($n => (is => 'rw', default => sub { 0 }));
$mop->method("inc_$n" => sub { $_[0]->$n($_[0]->$n + 1) });
};
package Thing;
use Moo;
use MooX::Role::Parameterized::With;
with Counter => { name => 'hits' };
Thing->new->inc_hits; # generates: hits attribute + inc_hits method
Module is marked experimental. role { } block runs at composition time; $mop proxies has/around/before/after/requires.
When Moose is loaded before Moo classes are compiled, Moo auto-inflates its metaclasses. This means:
extends a Moo classwith a Moose roleBEGIN { require Moose }
package MyMooseClass;
use Moose;
extends 'MyMooClass'; # works if Moose was loaded first
For Moose-style syntax in Moo (isa => 'Str', lazy_build), use MooX::late. Avoid Any::Moose – deprecated, points to Moo.
Moo has no built-in type system. isa takes a coderef:
use Types::Standard qw(Str Int ArrayRef);
has name => (is => 'ro', isa => Str);
has tags => (is => 'ro', isa => ArrayRef[Str], default => sub { [] });
Type::Tiny / Types::Standard is the official recommendation in Moo docs (replaces MooseX::Types).
| Situation | Use |
|---|---|
| Shared attributes/methods, stable "is-a" | extends |
| Optional/horizontal feature | Moo::Role + with |
| Same pattern, different config | MooX::Role::Parameterized |
| Delegate method set to sub-object | handles |
| Array/Hash operations on attribute | Sub::HandlesVia |
| Logging/validation/caching wrapper | before/around/after |
| Catch constructor typos | MooX::StrictConstructor |
| Cross-project boilerplate | Import::Into house-style module |
| Named types | Type::Tiny / Types::Standard |
| Multiple roles define same method | Sequential with or refactor |
| Legacy non-Moo parent | FOREIGNBUILDARGS |
| Multiple inheritance | Last resort; use mro 'c3' |
default => [] → shared state bug. Always default => sub { [] }.extends 'A'; extends 'B' → replaces, does NOT add B to A. Use extends 'A', 'B'.use Moo::Role are composed into consumers as methods.namespace::autoclean < 0.16 inflates Moo classes to Moose unexpectedly.trigger does NOT receive old value (unlike Moose).Sub::HandlesVia must be loaded after use Moo.BUILD chain is automatic; calling SUPER::BUILD manually breaks it.DESTROY; use DEMOLISH.