Catalog
affaan-m/perl-patterns

affaan-m

perl-patterns

Modern Perl 5.36+ idioms, best practices, and conventions for building robust, maintainable Perl applications.

global
origin:ECC
New~2.8k
v1.2Saved Jul 14, 2026

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 context

4. 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;  # 5

Regular 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 string

Path::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 config

Exporter 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 strings

perlcritic 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 -1

Dependency 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.

Files1
1 files · 1.0 KB

Select a file to preview

Overall Score

88/100

Grade

A

Excellent

Safety

92

Quality

87

Clarity

88

Completeness

84

Summary

This skill teaches modern Perl 5.36+ patterns, idioms, and best practices for writing robust, maintainable applications. It covers core language features (signatures, postfix dereferencing, context sensitivity), error handling (eval/die, Try::Tiny, native try/catch), OO patterns (Moo, native classes), regex practices, data structures, file I/O, module organization, and tooling (perltidy, perlcritic, carton). The skill is primarily instructional—it provides code examples and guidance for code review and refactoring, not executable operations.

Static Analysis Findings

1 finding

Patterns detected by deterministic static analysis before AI scoring. Hover over any finding code for detailed information and remediation guidance.

Command Injection
SEC-011Dynamic Shell Eval2x in 1 file

Shell eval/exec of dynamic content

SKILL.mdeval "2x

Detected Capabilities

code example provisionpattern documentationconfiguration file guidancebest practice instructionlegacy code comparison

Trigger Keywords

Phrases that MCP clients use to match this skill to user intent.

modernize perl codeperl best practicesperl 5.36 patternsrefactor perl legacyperl code reviewmoo object design

Risk Signals

INFO

Shell eval of dynamic content in Anti-Patterns section

SKILL.md:329-333 (eval/module loading anti-pattern)
INFO

eval construct used in Error Handling section

SKILL.md:110-115 (eval/die pattern for safe file reads)

Use Cases

  • Upgrade legacy Perl code to modern 5.36+ idioms
  • Review Perl code for adherence to modern best practices
  • Design Perl module architecture following idiomatic patterns
  • Teach developers modern error handling and object-oriented approaches in Perl
  • Configure tooling (perltidy, perlcritic) for consistent code standards
  • Refactor pre-5.36 Perl applications to use signatures and streamlined syntax

Quality Notes

  • Excellent structural organization with clear sections (Core Principles, Error Handling, OO patterns, etc.)
  • Comprehensive examples pairing good vs. bad patterns side-by-side, making trade-offs explicit
  • Concrete use cases (when to activate) clearly stated in opening section
  • Well-documented anti-patterns section explicitly flags risky constructs with explanations
  • Quick reference table provides fast lookup of legacy-to-modern mappings
  • Module organization includes practical project layout template and tooling configs
  • Clear scope: instructional guidance for code review and refactoring, not runtime execution
  • eval usage shown in SAFE context (eval block for exception handling) and UNSAFE context (string eval for module loading), with secure alternatives provided
  • No credential access, file writes, or destructive operations—read-only guidance skill
Model: claude-haiku-4-5-20251001Analyzed: Jul 14, 2026

Reviews

Add this skill to your library to leave a review.

No reviews yet

Be the first to share your experience.

Version History

v1.2

Content updated

2026-07-14

Latest
v1.1

Content updated

2026-04-20

v1.0

Seeded from github.com/affaan-m/everything-claude-code

2026-03-16

Use affaan-m/perl-patterns in your dev environment

Command Palette

Search for a command to run...