Intermediate Level

Perl Code
Examples

Idiomatic patterns, modules, error handling, and real-world techniques — all correct and debugged.

Functional Patterns
01 Dispatch tables (hash of coderefs)
coderefs dispatch
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
errors eval
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
closures functional
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
file i/o parsing
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
modules lists
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
sorting performance
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
formatting
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 oop
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 modules
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
cli modules
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
regex advanced
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 databases
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