Functional Patterns
01
Dispatch tables (hash of coderefs)
A dispatch table maps string keys to anonymous subroutines. This replaces long
if/elsif chains and is faster, extensible, and idiomatic Perl. Coderefs are first-class values — store them in hashes, pass them to subs, return them.
use strict; use warnings; # Each value is an anonymous sub (coderef) my %calc = ( add => sub { $_[0] + $_[1] }, sub => sub { $_[0] - $_[1] }, mul => sub { $_[0] * $_[1] }, div => sub { die "Division by zero\n" if $_[1] == 0; $_[0] / $_[1]; }, pow => sub { $_[0] ** $_[1] }, ); # Dispatch: look up the coderef and call it with ->() sub calculate { my ($op, $a, $b) = @_; die "Unknown op: $op\n" unless exists $calc{$op}; return $calc{$op}->($a, $b); } for my $test ( [add, 10, 3], [sub, 10, 3], [mul, 10, 3], [div, 10, 3], [pow, 2, 8], ) { my ($op, $a, $b) = @$test; printf "%3s(%2d, %d) = %s\n", $op, $a, $b, calculate($op, $a, $b); } # Dynamically add a new operation at runtime $calc{mod} = sub { $_[0] % $_[1] }; printf "mod(10, 3) = %d\n", calculate('mod', 10, 3);
Output
add(10, 3) = 13 sub(10, 3) = 7 mul(10, 3) = 30 div(10, 3) = 3.33333333333333 pow( 2, 8) = 256 mod(10, 3) = 1
02
Error handling — eval, die, and exception objects
Perl uses
eval { } as a try block and $@ to catch errors. For richer exceptions, die with a blessed object (or use Carp for better stack traces). Always check $@ immediately after eval — it can be cleared by other operations.
use strict; use warnings; use Carp qw(croak confess); # --- Simple eval/die --- sub divide { my ($a, $b) = @_; croak "Cannot divide by zero" if $b == 0; return $a / $b; } eval { divide(10, 0) }; if (my $err = $@) { print "Caught: $err\n"; } # --- Exception objects (lightweight, no module needed) --- package MyException; sub new { my ($class, %args) = @_; return bless { message => $args{message}, code => $args{code} // 0 }, $class; } sub message { return $_[0]->{message} } sub code { return $_[0]->{code} } sub stringify { my $self = shift; return "[Error ${\$self->code}] ${\$self->message}"; } package main; sub risky_op { my ($val) = @_; die MyException->new( message => "Value $val out of range", code => 404, ) if $val < 0; return sqrt($val); } # Distinguish object exceptions from string exceptions eval { risky_op(-4) }; if (my $e = $@) { if (ref $e && $e->isa('MyException')) { printf "Object exception: %s\n", $e->stringify(); } else { print "String exception: $e"; } } # Nested eval — inner errors don't escape eval { print "sqrt(9) = " . risky_op(9) . "\n"; print "sqrt(25) = " . risky_op(25) . "\n"; }; warn $@ if $@; # no error expected
Output
Caught: Cannot divide by zero at script.pl line 6. Object exception: [Error 404] Value -4 out of range sqrt(9) = 3 sqrt(25) = 5
03
Closures — factories and encapsulated state
A closure is a sub that captures variables from its enclosing scope. This is how Perl implements private state, factory functions, iterators, and partial application — without needing a class.
use strict; use warnings; # --- Counter factory: each call creates independent state --- sub make_counter { my ($start, $step) = @_; $step //= 1; my $count = $start; # captured by closure return { next => sub { return $count += $step }, reset => sub { $count = $start }, value => sub { return $count }, }; } my $c1 = make_counter(0, 1); my $c2 = make_counter(100, -10); $c1->{next}->() for 1..3; print "c1 after 3 steps: " . $c1->{value}->() . "\n"; $c2->{next}->() for 1..4; print "c2 after 4 steps: " . $c2->{value}->() . "\n"; # --- Partial application (currying) --- sub make_multiplier { my ($factor) = @_; return sub { return $_[0] * $factor }; } my $double = make_multiplier(2); my $triple = make_multiplier(3); my @nums = (1..5); my @doubled = map { $double->($_) } @nums; my @tripled = map { $triple->($_) } @nums; print "doubled: " . join(", ", @doubled) . "\n"; print "tripled: " . join(", ", @tripled) . "\n"; # --- Memoization with a closure --- sub memoize { my ($fn) = @_; my %cache; # private to this closure return sub { my $key = join(',', @_); return $cache{$key} if exists $cache{$key}; return $cache{$key} = $fn->(@_); }; } my $slow_sq = sub { return $_[0] ** 2 }; my $fast_sq = memoize($slow_sq); print "7^2 = " . $fast_sq->(7) . " (computed)\n"; print "7^2 = " . $fast_sq->(7) . " (cached)\n";
Output
c1 after 3 steps: 3 c2 after 4 steps: 60 doubled: 2, 4, 6, 8, 10 tripled: 3, 6, 9, 12, 15 7^2 = 49 (computed) 7^2 = 49 (cached)
Data Processing
04
CSV file processing without a module
Parsing and aggregating structured data from a file is a classic Perl task. This demonstrates reading CSV, building a hash-of-arrays for grouping, computing statistics, and writing a summary report — all with idiomatic Perl.
use strict; use warnings; use List::Util qw(sum min max); # Write sample CSV to a temp file my $csv_data = <<'END'; name,dept,salary Alice,Engineering,95000 Bob,HR,62000 Carol,Engineering,105000 Dave,Marketing,71000 Eve,HR,68000 Frank,Engineering,88000 Grace,Marketing,75000 END open(my $tmp, '>', '/tmp/staff.csv') or die $!; print $tmp $csv_data; close $tmp; # Read and parse, skip header open(my $fh, '<', '/tmp/staff.csv') or die $!; my $header = <$fh>; # consume header line chomp $header; my @fields = split /,/, $header; my %by_dept; # dept => [ {record}, ... ] while (my $line = <$fh>) { chomp $line; next unless $line =~ /\S/; # skip blank lines # Build a record hash from field names + values my %rec; @rec{@fields} = split /,/, $line; # hash slice assignment push @{$by_dept{ $rec{dept} }}, \%rec; } close $fh; # Print department summary printf "%-16s %5s %8s %8s %8s\n", "Department", "Count", "Min", "Max", "Avg"; print "-" x 52 . "\n"; for my $dept (sort keys %by_dept) { my @salaries = map { $_->{salary} } @{$by_dept{$dept}}; my $avg = sum(@salaries) / scalar(@salaries); printf "%-16s %5d %8d %8d %8.0f\n", $dept, scalar(@salaries), min(@salaries), max(@salaries), $avg; }
Output
Department Count Min Max Avg ---------------------------------------------------- Engineering 3 88000 105000 96000 HR 2 62000 68000 65000 Marketing 2 71000 75000 73000
05
List::Util — the essential list toolkit
List::Util (core module, always available) provides sum, min, max, first, any, all, none, reduce, uniq, and more. Know these — they eliminate hand-written loops.
use strict; use warnings; use List::Util qw( sum sum0 product min max minstr maxstr first any all none reduce uniq uniqstr shuffle pairs ); my @nums = (3, 1, 4, 1, 5, 9, 2, 6); my @words = ('banana', 'apple', 'cherry', 'date'); # Numeric aggregation printf "sum=%-4d product=%-6d min=%d max=%d\n", sum(@nums), product(@nums), min(@nums), max(@nums); # String min/max (uses cmp not <=>) printf "minstr=%s maxstr=%s\n", minstr(@words), maxstr(@words); # first — returns first matching element my $big = first { $_ > 5 } @nums; print "first > 5: $big\n"; # any / all / none — boolean tests print "any even? " . (any { $_ % 2 == 0 } @nums ? "yes" : "no") . "\n"; print "all > 0? " . (all { $_ > 0 } @nums ? "yes" : "no") . "\n"; print "none neg? " . (none { $_ < 0 } @nums ? "yes" : "no") . "\n"; # reduce — fold left with accumulator in $a, current in $b my $gcd; $gcd = reduce { my ($x, $y) = ($a, $b); ($x, $y) = ($y, $x % $y) while $y; $x; } 48, 36, 24; print "GCD(48,36,24) = $gcd\n"; # uniq — removes consecutive duplicates (sort first for all dups) my @unique = uniq sort {$a<=>$b} @nums; print "uniq sorted: " . join(" ", @unique) . "\n"; # pairs — iterate list as key-value pairs my @kv = (a => 1, b => 2, c => 3); for my $p (pairs @kv) { print " $p->[0] => $p->[1]\n"; }
Output
sum=31 product=6480 min=1 max=9 minstr=apple maxstr=date first > 5: 9 any even? yes all > 0? yes none neg? yes GCD(48,36,24) = 12 uniq sorted: 1 2 3 4 5 6 9 a => 1 b => 2 c => 3
06
Schwartzian transform — efficient complex sorting
When a sort key is expensive to compute, the Schwartzian transform computes it once per element (map → sort → map). This is idiomatic Perl and significantly faster than calling the key function inside
sort.
use strict; use warnings; my @files = ( "report10.txt", "report2.txt", "report1.txt", "report20.txt", "notes.txt", "archive.txt", ); # --- Problem: plain string sort gives wrong numeric order --- my @str_sorted = sort @files; print "String sort: " . join(", ", @str_sorted) . "\n"; # --- Schwartzian: decorate → sort → undecorate --- # Step 1 (map): wrap each element as [$original, $computed_key] # Step 2 (sort): compare only on the precomputed key # Step 3 (map): unwrap back to original my @nat_sorted = map { $_->[0] } # 3. unwrap sort { $a->[1] cmp $b->[1] # compare alpha prefix || $a->[2] <=> $b->[2] # then numeric suffix } map { # 1. decorate my ($prefix, $num) = $_ =~ /^([a-z]+)(\d*)/i; [$_, $prefix, $num || 0] } @files; print "Natural sort: " . join(", ", @nat_sorted) . "\n"; # --- Sort objects by computed score (the classic use case) --- my @people = ( { name => "Alice", scores => [90,85,92] }, { name => "Bob", scores => [70,88,75] }, { name => "Carol", scores => [95,91,89] }, ); use List::Util qw(sum); my @ranked = map { $_->[0] } sort { $b->[1] <=> $a->[1] } # descending avg map { my $avg = sum(@{$_->{scores}}) / scalar(@{$_->{scores}}); [$_, $avg] # [$original, $key] } @people; for my $p (@ranked) { my $avg = sum(@{$p->{scores}}) / scalar(@{$p->{scores}}); printf " %-8s avg=%.1f\n", $p->{name}, $avg; }
Output
String sort: archive.txt, notes.txt, report1.txt, report10.txt, report2.txt, report20.txt Natural sort: archive.txt, notes.txt, report1.txt, report2.txt, report10.txt, report20.txt Carol avg=91.7 Alice avg=89.0 Bob avg=77.7
Modules & OOP
07
sprintf / printf — formatted output mastery
sprintf and printf are workhorses for producing aligned, numeric, and structured text output. Understanding width, precision, padding, and format codes is essential for report generation and log output.
use strict; use warnings; use POSIX qw(floor ceil); # Integers: %d width zero-pad sign printf "%d\n", 42; # 42 printf "%8d\n", 42; # 42 (right-aligned, width 8) printf "%-8d|\n", 42; # 42 | (left-aligned) printf "%08d\n", 42; # 00000042 (zero-padded) printf "%+d %+d\n", 42, -3; # +42 -3 (force sign) print "---\n"; # Floats: %f %e %g precision printf "%.2f\n", 3.14159; # 3.14 printf "%10.3f\n", 3.14159; # 3.142 printf "%e\n", 123456.78; # 1.234568e+05 printf "%g\n", 0.0001; # 0.0001 printf "%g\n", 0.00001; # 1e-05 (%g picks shorter form) print "---\n"; # Strings: %s hex: %x octal: %o binary: %b printf "%s and %-10s|\n", "hello", "world"; printf "hex=0x%04X oct=0%o bin=%08b\n", 255, 255, 255; print "---\n"; # sprintf into a variable; build a table my @data = ( ["Widget A", 1024, 9.99], ["Gadget B", 357, 24.50], ["Doohickey", 89, 149.00], ); printf "%-12s %6s %9s %12s\n", "Item", "Units", "Price", "Total"; print "-" x 44 . "\n"; my $grand = 0; for my $row (@data) { my ($name, $qty, $price) = @$row; my $total = $qty * $price; $grand += $total; printf "%-12s %6d %9.2f %12.2f\n", $name, $qty, $price, $total; } print "-" x 44 . "\n"; printf "%-12s %17s %12.2f\n", "TOTAL", "", $grand;
Output
42
42
42 |
00000042
+42 -3
---
3.14
3.142
1.234568e+05
0.0001
1e-05
---
hello and world |
hex=0x00FF oct=0377 bin=11111111
---
Item Units Price Total
--------------------------------------------
Widget A 1024 9.99 10229.76
Gadget B 357 24.50 8746.50
Doohickey 89 149.00 13261.00
--------------------------------------------
TOTAL 32237.26
08
Moose — modern object-oriented Perl
Moose is the standard OOP framework for modern Perl. It provides declarative attributes with types and defaults, roles (like interfaces + mixins), method modifiers (before/after/around), and more — eliminating boilerplate bless/accessor code.
use strict; use warnings; ### --- Role: anything printable --- package Printable; use Moose::Role; requires 'to_string'; # classes that use this role must implement it sub print_self { my $self = shift; print $self->to_string() . "\n"; } ### --- Base class: Animal --- package Animal; use Moose; with 'Printable'; # Attribute: name — required, read-only string has 'name' => ( is => 'ro', isa => 'Str', required => 1, ); # Attribute: sound — has a default value has 'sound' => ( is => 'rw', isa => 'Str', default => '...', ); sub speak { my $s = shift; print $s->name." says: ".$s->sound."\n" } sub to_string { my $s = shift; return "Animal[".$s->name."]" } ### --- Subclass: Dog --- package Dog; use Moose; extends 'Animal'; # Override default for sound has '+sound' => ( default => 'Woof!' ); # Extra attribute with type constraint has 'breed' => ( is => 'ro', isa => 'Str', default => 'Mixed', ); # Method modifier: runs BEFORE speak() before 'speak' => sub { my $self = shift; print "[Dog wags tail]\n"; }; sub to_string { my $s = shift; return "Dog[".$s->name.", ".$s->breed."]"; } ### --- main --- package main; my $a = Animal->new(name => 'Parrot', sound => 'Squawk!'); my $d = Dog->new(name => 'Rex', breed => 'Labrador'); $a->speak(); $a->print_self(); # from the Printable role $d->speak(); # triggers 'before' modifier first $d->print_self(); # Type checking — this would die: Dog->new(name => 42) print "Rex isa Dog: " . ($d->isa('Dog') ? "yes" : "no") . "\n"; print "Rex isa Animal: " . ($d->isa('Animal') ? "yes" : "no") . "\n";
Output
Parrot says: Squawk! Animal[Parrot] [Dog wags tail] Rex says: Woof! Dog[Rex, Labrador] Rex isa Dog: yes Rex isa Animal: yes
Practical & Real-World
09
JSON::PP — parse and generate JSON
JSON::PP is a pure-Perl JSON module included in Perl's core since 5.14. Use JSON::XS for performance-critical code. This shows encoding, decoding, pretty-printing, and working with nested structures.
use strict; use warnings; use JSON::PP; my $json = JSON::PP->new->utf8->pretty->canonical; # utf8: encode/decode UTF-8 bytes # pretty: formatted output # canonical: sort hash keys (reproducible output) # --- Encode: Perl structure → JSON string --- my %config = ( app => "MyApp", version => "2.1.0", debug => JSON::PP::true, # JSON true/false/null timeout => JSON::PP::null, ports => [8080, 8443], db => { host => "localhost", port => 5432 }, ); my $json_str = $json->encode(\%config); print "Encoded:\n$json_str\n"; # --- Decode: JSON string → Perl structure --- my $raw = '{"users":[{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false}]}'; my $data = $json->decode($raw); # Traverse the decoded structure for my $user (@{ $data->{users} }) { my $status = $user->{active} ? "active" : "inactive"; printf " id=%-2d %-8s %s\n", $user->{id}, $user->{name}, $status; } # Safe decode with eval my $parsed = eval { $json->decode('{"bad": json}') }; if ($@) { print "Parse error caught: invalid JSON\n"; }
Output
Encoded:
{
"app" : "MyApp",
"db" : { "host" : "localhost", "port" : 5432 },
"debug" : true,
"ports" : [ 8080, 8443 ],
"timeout" : null,
"version" : "2.1.0"
}
id=1 Alice active
id=2 Bob inactive
Parse error caught: invalid JSON
10
Getopt::Long — command-line argument parsing
Getopt::Long (core) parses --long-option style arguments with full support for types, defaults, required args, and auto-help. Essential for any command-line script.
use strict; use warnings; use Getopt::Long; use Pod::Usage; # Declare option variables with defaults my $input = '-'; # stdin by default my $output = '-'; # stdout by default my $verbose = 0; my $count = 10; my @tags; # accumulate: --tag a --tag b my $help = 0; # Simulate command-line args (normally come from @ARGV) local @ARGV = ( '--input', 'data.csv', '--output', 'out.txt', '--count', '25', '--tag', 'perl', '--tag', 'scripting', '--verbose', ); GetOptions( 'input=s' => \$input, # =s string 'output=s' => \$output, 'count=i' => \$count, # =i integer 'tag=s' => \@tags, # accumulate into array 'verbose|v' => \$verbose, # boolean flag (--verbose or -v) 'help|h' => \$help, ) or die "Usage error. Try --help\n"; # pod2usage would print --help text from POD; we just print manually here if ($help) { print "Usage: script.pl --input FILE --output FILE [--count N] [--tag TAG...]\n"; exit 0; } # Use the parsed values printf "input: %s\n", $input; printf "output: %s\n", $output; printf "count: %d\n", $count; printf "verbose: %s\n", $verbose ? "yes" : "no"; printf "tags: %s\n", join(", ", @tags);
Output
input: data.csv output: out.txt count: 25 verbose: yes tags: perl, scripting
11
Advanced regular expressions
Lookahead/lookbehind, non-greedy quantifiers, backreferences,
tr/// (transliteration), split with limits, and building dynamic patterns with qr//. These separate intermediate Perl from beginner Perl.
use strict; use warnings; # --- Lookahead / lookbehind (zero-width assertions) --- my $text = "100USD 200EUR 350GBP 75USD"; # Positive lookahead: numbers followed by USD my @usd = ($text =~ /(\d+)(?=USD)/g); print "USD amounts: " . join(", ", @usd) . "\n"; # Positive lookbehind: currency code preceded by digits my @currencies = ($text =~ /(?<=\d)([A-Z]{3})/g); print "Currencies: " . join(", ", @currencies) . "\n"; # --- Non-greedy quantifiers --- my $html = "<b>bold</b> and <i>italic</i>"; # Greedy: matches from first < to last > $html =~ /(<.+>)/; print "Greedy: $1\n"; # Non-greedy +? : shortest match my @tags = ($html =~ /(<.+?>)/g); print "Non-greedy: " . join(" ", @tags) . "\n"; # --- Backreferences: match repeated words --- my $sentence = "the the quick brown fox fox jumped"; (my $fixed = $sentence) =~ s/\b(\w+)\s+\1\b/$1/gi; print "Deduped: $fixed\n"; # --- tr/// (transliteration) --- my $str = "Hello, World! 123"; (my $roted = $str) =~ tr/A-Za-z/N-ZA-Mn-za-m/; # ROT13 print "ROT13: $roted\n"; my $digit_count = ($str =~ tr/0-9//); # tr returns count print "Digits: $digit_count\n"; # --- qr// compiled regex as a value --- my @patterns = ( qr/^\d+$/, # all digits qr/^[a-z]+$/i, # all alpha qr/^\w+\@\w+\.\w+$/, # simple email ); for my $candidate ("42", "hello", "user\@x.com", "mixed123") { my $match = (grep { $candidate =~ $_ } @patterns)[0] // "none"; printf " %-12s matched: %s\n", $candidate, $match; }
Output
USD amounts: 100, 75 Currencies: USD, EUR, GBP, USD Greedy: <b>bold</b> and <i>italic</i> Non-greedy: <b> </b> <i> </i> Deduped: the quick brown fox jumped ROT13: Uryyb, Jbeyq! 123 Digits: 3 42 matched: (?^:^\d+$) hello matched: (?^i:^[a-z]+$) user@x.com matched: (?^:^\w+@\w+\.\w+$) mixed123 matched: none
12
DBI — database access
DBI (DataBase Interface) is the standard Perl database abstraction layer. This shows the complete pattern: connect, prepare, bind params, execute, fetch rows, transactions, and placeholders (which prevent SQL injection).
use strict; use warnings; use DBI; # Connect to SQLite (in-memory — requires DBD::SQLite) my $dbh = DBI->connect( 'dbi:SQLite:dbname=:memory:', '', '', { RaiseError => 1, # die on errors (don't check $dbh->err) AutoCommit => 1, # auto-commit each statement PrintError => 0, } ) or die DBI->errstr; # Create table $dbh->do('CREATE TABLE employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, dept TEXT, salary REAL )'); # Prepare + execute with placeholders (safe — prevents SQL injection) my $ins = $dbh->prepare( 'INSERT INTO employees (name, dept, salary) VALUES (?, ?, ?)' ); my @staff = ( ['Alice', 'Eng', 95000], ['Bob', 'HR', 62000], ['Carol', 'Eng', 105000], ['Dave', 'HR', 68000], ); # Transaction: all-or-nothing batch insert eval { $dbh->{AutoCommit} = 0; $ins->execute(@$_) for @staff; $dbh->commit; }; if ($@) { $dbh->rollback; die "Insert failed: $@" } $dbh->{AutoCommit} = 1; # SELECT with a placeholder filter my $sth = $dbh->prepare( 'SELECT name, dept, salary FROM employees WHERE salary > ? ORDER BY salary DESC' ); $sth->execute(60000); printf "%-10s %-6s %8s\n", "Name", "Dept", "Salary"; print "-" x 28 . "\n"; # fetchrow_hashref — each row as a hash ref while (my $row = $sth->fetchrow_hashref) { printf "%-10s %-6s %8.0f\n", $row->{name}, $row->{dept}, $row->{salary}; } # Aggregate query my ($count, $avg) = $dbh->selectrow_array( 'SELECT COUNT(*), AVG(salary) FROM employees' ); printf "\n%d employees, avg salary: \$%.0f\n", $count, $avg; $dbh->disconnect;
Output
Name Dept Salary ---------------------------- Carol Eng 105000 Alice Eng 95000 Dave HR 68000 Bob HR 62000 4 employees, avg salary: $82500