perl-patterns
Skill272 starsChanged 7 months agoMITView on GitHub
Modern Perl 5.36+ idioms, best practices, and conventions for building robust, maintainable Perl applications.
We split this skill into the rules it gives an agent
3 rules. 3 are common, 0 are distinctive, and 0 are very specific to this project.
3
Common
Most of the 4 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Common: line 51
Use signatures for clarity and automatic arity checking.
- Common: line 95
Use postfix dereference syntax for readability with nested structures.
- Common: line 178
Prefer Moo for lightweight, modern OO.
“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Modern Perl Development Patterns
Idiomatic Perl 5.36+ patterns and best practices for building robust, maintainable applications.
When to Activate
- Writing new Perl code or modules
- Reviewing Perl code for idiom compliance
- Refactoring legacy Perl to modern standards
- Designing Perl module architecture
- Migrating pre-5.36 code to modern Perl
How It Works
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.
Core Principles
1. Use v5.36 Pragma
A single use v5.36 replaces the old boilerplate and enables strict, warnings, and subroutine signatures.
# 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!";
}2. Subroutine Signatures
Use signatures for clarity and automatic arity checking.
use 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;
# ...
}3. Context Sensitivity
Understand scalar vs list context — a core Perl concept.
use 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 context4. Postfix Dereferencing
Use postfix dereference syntax for readability with nested structures.
use 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} };5. The isa Operator (5.32+)
Infix type-check — replaces blessed($o) && $o->isa('X').
use v5.36;
if ($obj isa 'My::Class') { $obj->do_something }Error Handling
eval/die Pattern
use v5.36;
sub parse_config($path) {
my $content = eval { path($path)->slurp_utf8 };
die "Config error: $@" if $@;
return decode_json($content);
}Try::Tiny (Reliable Exception Handling)
use 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;
}Native try/catch (5.40+)
use v5.40;
sub divide($x, $y) {
try {
die "Division by zero" if $y == 0;
return $x / $y;
}
catch ($e) {
warn "Error: $e";
return;
}
}Modern OO with Moo
Prefer Moo for lightweight, modern OO. Use Moose only when its metaprotocol is needed.
# 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;Moo Roles
package 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;Native class Keyword (5.38+, Corinna)
use 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; # 5Regular Expressions
Named Captures and /x Flag
use 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";
}Precompiled Patterns
use 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;
}Data Structures
References and Safe Deep Access
use 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";
}File I/O
Three-Argument Open
use 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 stringPath::Tiny for File Operations
use 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;
}Module Organization
Standard Project Layout
MyApp/
├── 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 configExporter Patterns
package 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;Tooling
perltidy Configuration (.perltidyrc)
-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 stringsperlcritic Configuration (.perlcriticrc)
severity = 3
theme = core + pbp + security
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
exclude_functions = say print
[Subroutines::ProhibitExplicitReturnUndef]
severity = 4
[ValuesAndExpressions::ProhibitMagicNumbers]
allowed_values = 0 1 2 -1Dependency Management (cpanfile + carton)
cpanm App::cpanminus Carton # Install tools
carton install # Install deps from cpanfile
carton exec -- perl bin/myapp # Run with local deps# cpanfile
requires 'Moo', '>= 2.005';
requires 'Path::Tiny';
requires 'JSON::MaybeXS';
requires 'Try::Tiny';
on test => sub {
requires 'Test2::V0';
requires 'Test::MockModule';
};Quick Reference: Modern Perl Idioms
| 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) |
Anti-Patterns
# 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.
Other versions10 projects use a version of this.
Word for word · 5
- SamuelNDCE/claude-super-skill-library★ 1100% same
- TironFox/everything-claude-code★ 1100% same
- davidanthonyn/claude-skills★ 0100% same
- MohamedEssam0-0/claude-skills★ 0100% same
- PeterYang888/claude-skills★ 0100% same
Edited · 5
- Fmarzochi/EGC★ 61100% same
- gongyijie85/dsh-ecc★ 899% same
- ronmkr/PromptBook★ 2100% same
- junimnjw/everything-claude-code★ 175% same
- renepf/claude-skills★ 0100% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository178 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .codex/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- examples/CLAUDE.mdCLAUDE.md
- .cursor/rules/common-agents.mdCursor rule
- .cursor/rules/common-coding-style.mdCursor rule
- .cursor/rules/common-development-workflow.mdCursor rule
- .cursor/rules/common-git-workflow.mdCursor rule
- .cursor/rules/common-hooks.mdCursor rule
- .cursor/rules/common-patterns.mdCursor rule
- .cursor/rules/common-performance.mdCursor rule
- .cursor/rules/common-security.mdCursor rule
- .cursor/rules/common-testing.mdCursor rule
- .cursor/rules/golang-coding-style.mdCursor rule
- .cursor/rules/golang-hooks.mdCursor rule
- .cursor/rules/golang-patterns.mdCursor rule
- .cursor/rules/golang-security.mdCursor rule
- .cursor/rules/golang-testing.mdCursor rule
- .cursor/rules/kotlin-coding-style.mdCursor rule
- .cursor/rules/kotlin-hooks.mdCursor rule
- .cursor/rules/kotlin-patterns.mdCursor rule
- .cursor/rules/kotlin-security.mdCursor rule
- .cursor/rules/kotlin-testing.mdCursor rule
- .cursor/rules/php-coding-style.mdCursor rule
- .cursor/rules/php-hooks.mdCursor rule
- .cursor/rules/php-patterns.mdCursor rule
- .cursor/rules/php-security.mdCursor rule
- .cursor/rules/php-testing.mdCursor rule
- .cursor/rules/python-coding-style.mdCursor rule
- .cursor/rules/python-hooks.mdCursor rule
- .cursor/rules/python-patterns.mdCursor rule
- .cursor/rules/python-security.mdCursor rule
- .cursor/rules/python-testing.mdCursor rule
- .cursor/rules/swift-coding-style.mdCursor rule
- .cursor/rules/swift-hooks.mdCursor rule
- .cursor/rules/swift-patterns.mdCursor rule
- .cursor/rules/swift-security.mdCursor rule
- .cursor/rules/swift-testing.mdCursor rule
- .cursor/rules/typescript-coding-style.mdCursor rule
- .cursor/rules/typescript-hooks.mdCursor rule
- .cursor/rules/typescript-patterns.mdCursor rule
- .cursor/rules/typescript-security.mdCursor rule
- .cursor/rules/typescript-testing.mdCursor rule
- llms.txtllms.txt
- article-writingSkill
- bun-runtimeSkill
- content-engineSkill
- documentation-lookupSkill
- frontend-slidesSkill
- investor-materialsSkill
- investor-outreachSkill
- market-researchSkill
- mcp-server-patternsSkill
- nextjs-turbopackSkill
- agent-evalSkill
- agent-harness-constructionSkill
- agentic-engineeringSkill
- ai-first-engineeringSkill
- ai-regression-testingSkill
- android-clean-architectureSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

