Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Modern Perl 5.36+ idioms, best practices, and conventions for building robust, maintainable Perl applications.
.claude/skills/loulanyue-perl-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 293% | 0% |
| case-23 | ✓→✗ | ▼ Worse | 300% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 171% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 328% | 0% |
Idiomatic Perl 5.36+ patterns and best practices for building robust, maintainable applications.
Apply these patterns as a bias toward modern Perl 5.36+ defaults: signatures, explicit modules, focused error handling, and testable boundaries. The examples below are meant to be copied as starting points, then tightened for the actual app, dependency stack, and deployment model in front of you.
v5.36 PragmaA single use v5.36 replaces the old boilerplate and enables strict, warnings, and subroutine signatures.
perl# Good: Modern preamble use v5.36; sub greet($name) { say "Hello, $name!"; } # Bad: Legacy boilerplate use strict; use warnings; use feature 'say', 'signatures'; no warnings 'experimental::signatures'; sub greet { my ($name) = @_; say "Hello, $name!"; }
Use signatures for clarity and automatic arity checking.
perluse v5.36; # Good: Signatures with defaults sub connect_db($host, $port = 5432, $timeout = 30) { # $host is required, others have defaults return DBI->connect("dbi:Pg:host=$host;port=$port", undef, undef, { RaiseError => 1, PrintError => 0, }); } # Good: Slurpy parameter for variable args sub log_message($level, @details) { say "[$level] " . join(' ', @details); } # Bad: Manual argument unpacking sub connect_db { my ($host, $port, $timeout) = @_; $port //= 5432; $timeout //= 30; # ... }
Understand scalar vs list context — a core Perl concept.
perluse v5.36; my @items = (1, 2, 3, 4, 5); my @copy = @items; # List context: all elements my $count = @items; # Scalar context: count (5) say "Items: " . scalar @items; # Force scalar context
Use postfix dereference syntax for readability with nested structures.
perluse v5.36; my $data = { users => [ { name => 'Alice', roles => ['admin', 'user'] }, { name => 'Bob', roles => ['user'] }, ], }; # Good: Postfix dereferencing my @users = $data->{users}->@*; my @roles = $data->{users}[0]{roles}->@*; my %first = $data->{users}[0]->%*; # Bad: Circumfix dereferencing (harder to read in chains) my @users = @{ $data->{users} }; my @roles = @{ $data->{users}[0]{roles} };
isa Operator (5.32+)Infix type-check — replaces blessed($o) && $o->isa('X').
perluse v5.36; if ($obj isa 'My::Class') { $obj->do_something }
perluse v5.36; sub parse_config($path) { my $content = eval { path($path)->slurp_utf8 }; die "Config error: $@" if $@; return decode_json($content); }
perluse v5.36; use Try::Tiny; sub fetch_user($id) { my $user = try { $db->resultset('User')->find($id) // die "User $id not found\n"; } catch { warn "Failed to fetch user $id: $_"; undef; }; return $user; }
perluse v5.40; sub divide($x, $y) { try { die "Division by zero" if $y == 0; return $x / $y; } catch ($e) { warn "Error: $e"; return; } }
Prefer Moo for lightweight, modern OO. Use Moose only when its metaprotocol is needed.
perl# Good: Moo class package User; use Moo; use Types::Standard qw(Str Int ArrayRef); use namespace::autoclean; has name => (is => 'ro', isa => Str, required => 1); has email => (is => 'ro', isa => Str, required => 1); has age => (is => 'ro', isa => Int, default => sub { 0 }); has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] }); sub is_admin($self) { return grep { $_ eq 'admin' } $self->roles->@*; } sub greet($self) { return "Hello, I'm " . $self->name; } 1; # Usage my $user = User->new( name => 'Alice', email => 'alice@example.com', roles => ['admin', 'user'], ); # Bad: Blessed hashref (no validation, no accessors) package User; sub new { my ($class, %args) = @_; return bless \%args, $class; } sub name { return $_[0]->{name} } 1;
perlpackage Role::Serializable; use Moo::Role; use JSON::MaybeXS qw(encode_json); requires 'TO_HASH'; sub to_json($self) { encode_json($self->TO_HASH) } 1; package User; use Moo; with 'Role::Serializable'; has name => (is => 'ro', required => 1); has email => (is => 'ro', required => 1); sub TO_HASH($self) { { name => $self->name, email => $self->email } } 1;
class Keyword (5.38+, Corinna)perluse v5.38; use feature 'class'; no warnings 'experimental::class'; class Point { field $x :param; field $y :param; method magnitude() { sqrt($x**2 + $y**2) } } my $p = Point->new(x => 3, y => 4); say $p->magnitude; # 5
/x Flagperluse v5.36; # Good: Named captures with /x for readability my $log_re = qr{ ^ (?<timestamp> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} ) \s+ \[ (?<level> \w+ ) \] \s+ (?<message> .+ ) $ }x; if ($line =~ $log_re) { say "Time: $+{timestamp}, Level: $+{level}"; say "Message: $+{message}"; } # Bad: Positional captures (hard to maintain) if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) { say "Time: $1, Level: $2"; }
perluse v5.36; # Good: Compile once, use many my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; sub validate_emails(@emails) { return grep { $_ =~ $email_re } @emails; }
perluse v5.36; # Hash and array references my $config = { database => { host => 'localhost', port => 5432, options => ['utf8', 'sslmode=require'], }, }; # Safe deep access (returns undef if any level missing) my $port = $config->{database}{port}; # 5432 my $missing = $config->{cache}{host}; # undef, no error # Hash slices my %subset; @subset{qw(host port)} = @{$config->{database}}{qw(host port)}; # Array slices my @first_two = $config->{database}{options}->@[0, 1]; # Multi-variable for loop (experimental in 5.36, stable in 5.40) use feature 'for_list'; no warnings 'experimental::for_list'; for my ($key, $val) (%$config) { say "$key => $val"; }
perluse v5.36; # Good: Three-arg open with autodie (core module, eliminates 'or die') use autodie; sub read_file($path) { open my $fh, '<:encoding(UTF-8)', $path; local $/; my $content = <$fh>; close $fh; return $content; } # Bad: Two-arg open (shell injection risk, see perl-security) open FH, $path; # NEVER do this open FH, "< $path"; # Still bad — user data in mode string
perluse v5.36; use Path::Tiny; my $file = path('config', 'app.json'); my $content = $file->slurp_utf8; $file->spew_utf8($new_content); # Iterate directory for my $child (path('src')->children(qr/\.pl$/)) { say $child->basename; }
textMyApp/ ├── lib/ │ └── MyApp/ │ ├── App.pm # Main module │ ├── Config.pm # Configuration │ ├── DB.pm # Database layer │ └── Util.pm # Utilities ├── bin/ │ └── myapp # Entry-point script ├── t/ │ ├── 00-load.t # Compilation tests │ ├── unit/ # Unit tests │ └── integration/ # Integration tests ├── cpanfile # Dependencies ├── Makefile.PL # Build system └── .perlcriticrc # Linting config
perlpackage MyApp::Util; use v5.36; use Exporter 'import'; our @EXPORT_OK = qw(trim); our %EXPORT_TAGS = (all => \@EXPORT_OK); sub trim($str) { $str =~ s/^\s+|\s+$//gr } 1;
text-i=4 # 4-space indent -l=100 # 100-char line length -ci=4 # continuation indent -ce # cuddled else -bar # opening brace on same line -nolq # don't outdent long quoted strings
iniseverity = 3 theme = core + pbp + security [InputOutput::RequireCheckedSyscalls] functions = :builtins exclude_functions = say print [Subroutines::ProhibitExplicitReturnUndef] severity = 4 [ValuesAndExpressions::ProhibitMagicNumbers] allowed_values = 0 1 2 -1
bashcpanm App::cpanminus Carton # Install tools carton install # Install deps from cpanfile carton exec -- perl bin/myapp # Run with local deps
perl# cpanfile requires 'Moo', '>= 2.005'; requires 'Path::Tiny'; requires 'JSON::MaybeXS'; requires 'Try::Tiny'; on test => sub { requires 'Test2::V0'; requires 'Test::MockModule'; };
| Legacy Pattern | Modern Replacement | |---|---| | use strict; use warnings; | use v5.36; | | my ($x, $y) = @_; | sub foo($x, $y) { ... } | | @{ $ref } | $ref->@* | | %{ $ref } | $ref->%* | | open FH, "< $file" | open my $fh, '<:encoding(UTF-8)', $file | | blessed hashref | Moo class with types | | $1, $2, $3 | $+{name} (named captures) | | eval { }; if ($@) | Try::Tiny or native try/catch (5.40+) | | BEGIN { require Exporter; } | use Exporter 'import'; | | Manual file ops | Path::Tiny | | blessed($o) && $o->isa('X') | $o isa 'X' (5.32+) | | builtin::true / false | use builtin 'true', 'false'; (5.36+, experimental) |
perl# 1. Two-arg open (security risk) open FH, $filename; # NEVER # 2. Indirect object syntax (ambiguous parsing) my $obj = new Foo(bar => 1); # Bad my $obj = Foo->new(bar => 1); # Good # 3. Excessive reliance on $_ map { process($_) } grep { validate($_) } @items; # Hard to follow my @valid = grep { validate($_) } @items; # Better: break it up my @results = map { process($_) } @valid; # 4. Disabling strict refs no strict 'refs'; # Almost always wrong ${"My::Package::$var"} = $value; # Use a hash instead # 5. Global variables as configuration our $TIMEOUT = 30; # Bad: mutable global use constant TIMEOUT => 30; # Better: constant # Best: Moo attribute with default # 6. String eval for module loading eval "require $module"; # Bad: code injection risk eval "use $module"; # Bad use Module::Runtime 'require_module'; # Good: safe module loading require_module($module);
Remember: Modern Perl is clean, readable, and safe. Let use v5.36 handle the boilerplate, use Moo for objects, and prefer CPAN's battle-tested modules over hand-rolled solutions.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | pass→pass | 14,091 | 13,937 | -1% | 1 | 1 | 0% | 2,279 | 6,168 | +171% | 0 | 0 | — |
case-01 | fail→fail | 16,226 | 12,814 | -21% | 1 | 1 | 0% | 3,207 | 6,547 | +104% | 0 | 0 | — |
case-03 | pass→pass | 6,385 | 5,039 | -21% | 1 | 1 | 0% | 1,088 | 4,653 | +328% | 0 | 0 | — |
case-04 | fail→pass | 12,070 | 5,639 | -53% | 1 | 1 | 0% | 2,146 | 4,727 | +120% | 0 | 0 | — |
case-05 | pass→pass | 5,196 | 3,989 | -23% | 1 | 1 | 0% | 944 | 4,345 | +360% | 0 | 0 | — |
case-06 | pass→pass | 6,778 | 3,133 | -54% | 1 | 1 | 0% | 1,153 | 4,206 | +265% | 0 | 0 | — |
case-07 | pass→pass | 9,498 | 8,100 | -15% | 1 | 1 | 0% | 1,629 | 5,164 | +217% | 0 | 0 | — |
case-08 | pass→pass | 9,779 | 4,763 | -51% | 1 | 1 | 0% | 1,737 | 4,584 | +164% | 0 | 0 | — |
case-09 | pass→pass | 9,301 | 4,561 | -51% | 1 | 1 | 0% | 1,590 | 4,604 | +190% | 0 | 0 | — |
case-10 | pass→pass | 12,686 | 8,266 | -35% | 1 | 1 | 0% | 2,325 | 5,388 | +132% | 0 | 0 | — |
case-11 | pass→pass | 6,629 | 7,464 | +13% | 1 | 1 | 0% | 1,292 | 5,151 | +299% | 0 | 0 | — |
case-12 | pass→pass | 13,932 | 9,331 | -33% | 1 | 1 | 0% | 2,697 | 5,428 | +101% | 0 | 0 | — |
case-13 | pass→pass | 11,898 | 8,102 | -32% | 1 | 1 | 0% | 1,955 | 5,183 | +165% | 0 | 0 | — |
case-14 | pass→pass | 14,883 | 7,026 | -53% | 1 | 1 | 0% | 2,661 | 5,026 | +89% | 0 | 0 | — |
case-15 | fail→pass | 7,750 | 7,230 | -7% | 1 | 1 | 0% | 1,279 | 5,022 | +293% | 0 | 0 | — |
case-16 | pass→pass | 5,272 | 5,135 | -3% | 1 | 1 | 0% | 984 | 4,737 | +381% | 0 | 0 | — |
case-17 | pass→pass | 13,936 | 10,782 | -23% | 1 | 1 | 0% | 2,370 | 5,760 | +143% | 0 | 0 | — |
case-18 | pass→pass | 7,392 | 5,729 | -22% | 1 | 1 | 0% | 1,316 | 4,729 | +259% | 0 | 0 | — |
case-19 | pass→pass | 5,535 | 4,944 | -11% | 1 | 1 | 0% | 867 | 4,517 | +421% | 0 | 0 | — |
case-20 | pass→pass | 3,225 | 2,769 | -14% | 1 | 1 | 0% | 581 | 4,281 | +637% | 0 | 0 | — |
case-21 | pass→pass | 11,473 | 8,242 | -28% | 1 | 1 | 0% | 2,045 | 5,111 | +150% | 0 | 0 | — |
case-22 | pass→pass | 4,213 | 5,207 | +24% | 1 | 1 | 0% | 682 | 4,625 | +578% | 0 | 0 | — |
case-23 | pass→fail | 5,873 | 6,393 | +9% | 1 | 1 | 0% | 1,223 | 4,887 | +300% | 0 | 0 | — |
case-24 | pass→pass | 9,158 | 8,076 | -12% | 1 | 1 | 0% | 1,666 | 5,120 | +207% | 0 | 0 | — |
case-25 | pass→pass | 11,393 | 8,572 | -25% | 1 | 1 | 0% | 2,094 | 5,361 | +156% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.