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.
perl
1# Good: Modern preamble
2use v5.36;
3
4sub greet($name) {
5 say "Hello, $name!";
6}
7
8# Bad: Legacy boilerplate
9use strict;
10use warnings;
11use feature 'say', 'signatures';
12no warnings 'experimental::signatures';
13
14sub greet {
15 my ($name) = @_;
16 say "Hello, $name!";
17}
2. Subroutine Signatures
Use signatures for clarity and automatic arity checking.
perl
1use v5.36;
2
3# Good: Signatures with defaults
4sub connect_db($host, $port = 5432, $timeout = 30) {
5 # $host is required, others have defaults
6 return DBI->connect("dbi:Pg:host=$host;port=$port", undef, undef, {
7 RaiseError => 1,
8 PrintError => 0,
9 });
10}
11
12# Good: Slurpy parameter for variable args
13sub log_message($level, @details) {
14 say "[$level] " . join(' ', @details);
15}
16
17# Bad: Manual argument unpacking
18sub connect_db {
19 my ($host, $port, $timeout) = @_;
20 $port //= 5432;
21 $timeout //= 30;
22 # ...
23}
3. Context Sensitivity
Understand scalar vs list context — a core Perl concept.
perl
1use v5.36;
2
3my @items = (1, 2, 3, 4, 5);
4
5my @copy = @items; # List context: all elements
6my $count = @items; # Scalar context: count (5)
7say "Items: " . scalar @items; # Force scalar context
4. Postfix Dereferencing
Use postfix dereference syntax for readability with nested structures.
perl
1use v5.36;
2
3my $data = {
4 users => [
5 { name => 'Alice', roles => ['admin', 'user'] },
6 { name => 'Bob', roles => ['user'] },
7 ],
8};
9
10# Good: Postfix dereferencing
11my @users = $data->{users}->@*;
12my @roles = $data->{users}[0]{roles}->@*;
13my %first = $data->{users}[0]->%*;
14
15# Bad: Circumfix dereferencing (harder to read in chains)
16my @users = @{ $data->{users} };
17my @roles = @{ $data->{users}[0]{roles} };
5. The isa Operator (5.32+)
Infix type-check — replaces blessed($o) && $o->isa('X').
perl
1use v5.36;
2if ($obj isa 'My::Class') { $obj->do_something }
Error Handling
eval/die Pattern
perl
1use v5.36;
2
3sub parse_config($path) {
4 my $content = eval { path($path)->slurp_utf8 };
5 die "Config error: $@" if $@;
6 return decode_json($content);
7}
Try::Tiny (Reliable Exception Handling)
perl
1use v5.36;
2use Try::Tiny;
3
4sub fetch_user($id) {
5 my $user = try {
6 $db->resultset('User')->find($id)
7 // die "User $id not found\n";
8 }
9 catch {
10 warn "Failed to fetch user $id: $_";
11 undef;
12 };
13 return $user;
14}
Native try/catch (5.40+)
perl
1use v5.40;
2
3sub divide($x, $y) {
4 try {
5 die "Division by zero" if $y == 0;
6 return $x / $y;
7 }
8 catch ($e) {
9 warn "Error: $e";
10 return;
11 }
12}
Modern OO with Moo
Prefer Moo for lightweight, modern OO. Use Moose only when its metaprotocol is needed.
perl
1# Good: Moo class
2package User;
3use Moo;
4use Types::Standard qw(Str Int ArrayRef);
5use namespace::autoclean;
6
7has name => (is => 'ro', isa => Str, required => 1);
8has email => (is => 'ro', isa => Str, required => 1);
9has age => (is => 'ro', isa => Int, default => sub { 0 });
10has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] });
11
12sub is_admin($self) {
13 return grep { $_ eq 'admin' } $self->roles->@*;
14}
15
16sub greet($self) {
17 return "Hello, I'm " . $self->name;
18}
19
201;
21
22# Usage
23my $user = User->new(
24 name => 'Alice',
25 email => 'alice@example.com',
26 roles => ['admin', 'user'],
27);
28
29# Bad: Blessed hashref (no validation, no accessors)
30package User;
31sub new {
32 my ($class, %args) = @_;
33 return bless \%args, $class;
34}
35sub name { return $_[0]->{name} }
361;
Moo Roles
perl
1package Role::Serializable;
2use Moo::Role;
3use JSON::MaybeXS qw(encode_json);
4requires 'TO_HASH';
5sub to_json($self) { encode_json($self->TO_HASH) }
61;
7
8package User;
9use Moo;
10with 'Role::Serializable';
11has name => (is => 'ro', required => 1);
12has email => (is => 'ro', required => 1);
13sub TO_HASH($self) { { name => $self->name, email => $self->email } }
141;
Native class Keyword (5.38+, Corinna)
perl
1use v5.38;
2use feature 'class';
3no warnings 'experimental::class';
4
5class Point {
6 field $x :param;
7 field $y :param;
8 method magnitude() { sqrt($x**2 + $y**2) }
9}
10
11my $p = Point->new(x => 3, y => 4);
12say $p->magnitude; # 5
Regular Expressions
Named Captures and /x Flag
perl
1use v5.36;
2
3# Good: Named captures with /x for readability
4my $log_re = qr{
5 ^ (?<timestamp> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} )
6 \s+ \[ (?<level> \w+ ) \]
7 \s+ (?<message> .+ ) $
8}x;
9
10if ($line =~ $log_re) {
11 say "Time: $+{timestamp}, Level: $+{level}";
12 say "Message: $+{message}";
13}
14
15# Bad: Positional captures (hard to maintain)
16if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) {
17 say "Time: $1, Level: $2";
18}
Precompiled Patterns
perl
1use v5.36;
2
3# Good: Compile once, use many
4my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
5
6sub validate_emails(@emails) {
7 return grep { $_ =~ $email_re } @emails;
8}
Data Structures
References and Safe Deep Access
perl
1use v5.36;
2
3# Hash and array references
4my $config = {
5 database => {
6 host => 'localhost',
7 port => 5432,
8 options => ['utf8', 'sslmode=require'],
9 },
10};
11
12# Safe deep access (returns undef if any level missing)
13my $port = $config->{database}{port}; # 5432
14my $missing = $config->{cache}{host}; # undef, no error
15
16# Hash slices
17my %subset;
18@subset{qw(host port)} = @{$config->{database}}{qw(host port)};
19
20# Array slices
21my @first_two = $config->{database}{options}->@[0, 1];
22
23# Multi-variable for loop (experimental in 5.36, stable in 5.40)
24use feature 'for_list';
25no warnings 'experimental::for_list';
26for my ($key, $val) (%$config) {
27 say "$key => $val";
28}
File I/O
Three-Argument Open
perl
1use v5.36;
2
3# Good: Three-arg open with autodie (core module, eliminates 'or die')
4use autodie;
5
6sub read_file($path) {
7 open my $fh, '<:encoding(UTF-8)', $path;
8 local $/;
9 my $content = <$fh>;
10 close $fh;
11 return $content;
12}
13
14# Bad: Two-arg open (shell injection risk, see perl-security)
15open FH, $path; # NEVER do this
16open FH, "< $path"; # Still bad — user data in mode string
Path::Tiny for File Operations
perl
1use v5.36;
2use Path::Tiny;
3
4my $file = path('config', 'app.json');
5my $content = $file->slurp_utf8;
6$file->spew_utf8($new_content);
7
8# Iterate directory
9for my $child (path('src')->children(qr/\.pl$/)) {
10 say $child->basename;
11}
Module Organization
Standard Project Layout
text
1MyApp/
2├── lib/
3│ └── MyApp/
4│ ├── App.pm # Main module
5│ ├── Config.pm # Configuration
6│ ├── DB.pm # Database layer
7│ └── Util.pm # Utilities
8├── bin/
9│ └── myapp # Entry-point script
10├── t/
11│ ├── 00-load.t # Compilation tests
12│ ├── unit/ # Unit tests
13│ └── integration/ # Integration tests
14├── cpanfile # Dependencies
15├── Makefile.PL # Build system
16└── .perlcriticrc # Linting config
Exporter Patterns
perl
1package MyApp::Util;
2use v5.36;
3use Exporter 'import';
4
5our @EXPORT_OK = qw(trim);
6our %EXPORT_TAGS = (all => \@EXPORT_OK);
7
8sub trim($str) { $str =~ s/^\s+|\s+$//gr }
9
101;
perltidy Configuration (.perltidyrc)
text
1-i=4 # 4-space indent
2-l=100 # 100-char line length
3-ci=4 # continuation indent
4-ce # cuddled else
5-bar # opening brace on same line
6-nolq # don't outdent long quoted strings
perlcritic Configuration (.perlcriticrc)
ini
1severity = 3
2theme = core + pbp + security
3
4[InputOutput::RequireCheckedSyscalls]
5functions = :builtins
6exclude_functions = say print
7
8[Subroutines::ProhibitExplicitReturnUndef]
9severity = 4
10
11[ValuesAndExpressions::ProhibitMagicNumbers]
12allowed_values = 0 1 2 -1
Dependency Management (cpanfile + carton)
bash
1cpanm App::cpanminus Carton # Install tools
2carton install # Install deps from cpanfile
3carton exec -- perl bin/myapp # Run with local deps
perl
1# cpanfile
2requires 'Moo', '>= 2.005';
3requires 'Path::Tiny';
4requires 'JSON::MaybeXS';
5requires 'Try::Tiny';
6
7on test => sub {
8 requires 'Test2::V0';
9 requires 'Test::MockModule';
10};
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
perl
1# 1. Two-arg open (security risk)
2open FH, $filename; # NEVER
3
4# 2. Indirect object syntax (ambiguous parsing)
5my $obj = new Foo(bar => 1); # Bad
6my $obj = Foo->new(bar => 1); # Good
7
8# 3. Excessive reliance on $_
9map { process($_) } grep { validate($_) } @items; # Hard to follow
10my @valid = grep { validate($_) } @items; # Better: break it up
11my @results = map { process($_) } @valid;
12
13# 4. Disabling strict refs
14no strict 'refs'; # Almost always wrong
15${"My::Package::$var"} = $value; # Use a hash instead
16
17# 5. Global variables as configuration
18our $TIMEOUT = 30; # Bad: mutable global
19use constant TIMEOUT => 30; # Better: constant
20# Best: Moo attribute with default
21
22# 6. String eval for module loading
23eval "require $module"; # Bad: code injection risk
24eval "use $module"; # Bad
25use Module::Runtime 'require_module'; # Good: safe module loading
26require_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.