Tutorial 01 — Core Language

The Perl Language
in Practice

A runnable program walking through every core language concept — scalars, arrays, hashes, control flow, loops, regex, subroutines, references, file I/O, OOP, and more.

perl perl_tutorial.pl

What This Tutorial Covers

  • Scalars, strings, numbers, and escape sequences
  • Arrays — push/pop/shift/unshift, grep, map, sort
  • Hashes — keys/values/each, frequency counts, grouping
  • Control flow — if/elsif/else, unless, ternary, defined-or
  • All loop types including labels and loop control
  • Regular expressions — match, substitute, capture groups
  • Subroutines, closures, factories, dispatch tables, recursion
  • References and nested data structures (AoH, HoA, deep nesting)
  • File I/O — read, write, append, slurp, file tests
  • Error handling with eval/$@
  • Object-oriented Perl with inheritance and polymorphism
  • List::Util, Scalar::Util, Data::Dumper
§03

Scalars — The Basic Variable

Every scalar variable starts with $. The my keyword declares it with lexical (block) scope. With use strict in effect, every variable must be declared.

perl
use strict;
use warnings;

my $name    = "Alice";
my $age     = 30;
my $pi      = 3.14159;
my $nothing = undef;

# Double-quotes interpolate; single-quotes are literal
my $city = "San Francisco";
print "Welcome to $city!\n";         # → Welcome to San Francisco!
print 'No interpolation: $city\n', "\n";  # literal

# Checking defined vs. undef
print defined($nothing) ? "defined" : "undef";    # → undef
Output
Welcome to San Francisco! No interpolation: $city\n undef
§04

Strings & String Operators

perl
my $full = "Hello" . ", " . "World!";   # . = concatenation
my $line = "-" x 40;                     # x = repetition

# String functions
my $sample = "  Perl Programming  ";
print length($sample);        # 20
print uc($sample);            # "  PERL PROGRAMMING  "
print ucfirst(lc("hELLO"));   # "Hello"
print substr("Submarine", 3, 4);   # "marin"
print index("Submarine", "arin");  # 4

# chomp removes the trailing newline
my $s = "Hello\n";
chomp $s;    # $s is now "Hello" (no newline)

# sprintf — formatted strings
printf "%-22s %s\n", "Zero-padded:",   sprintf("%08d", 42);    # 00000042
printf "%-22s %s\n", "Float:",         sprintf("%.4f", 3.14);  # 3.1400
printf "%-22s %s\n", "Left-aligned:",  sprintf("%-10s|", "hi"); # "hi        |"

# Here-doc — multi-line string
my $poem = <<END_POEM;
Roses are red,
Violets are blue,
Perl is awesome.
END_POEM

String comparison uses word operators: eq ne lt gt le ge cmp

Never use == to compare strings — it coerces them to numbers first. Use eq for string equality and == only for numeric equality.
§05

Numbers & Numeric Operators

perl
use POSIX qw(floor ceil);

# Literal forms
my $hex = 0xFF;        # 255
my $oct = 0755;        # 493
my $bin = 0b1010;      # 10
my $big = 1_000_000;   # underscores for readability

# Arithmetic
printf "17 + 5  = %d\n",   17 + 5;
printf "17 % 5  = %d\n",   17 % 5;   # modulus
printf "2 ** 10 = %d\n",   2 ** 10;  # exponent → 1024

# Math functions
print abs(-42),    "\n";   # 42
print int(3.9),    "\n";   # 3  (truncate, not round)
print sqrt(144),   "\n";   # 12
print floor(3.7),  "\n";   # 3
print ceil(3.2),   "\n";   # 4

# Numeric sort (must use $a <=> $b)
my @nums    = (5, 2, 9, 1, 7, 3);
my @sorted  = sort { $a <=> $b } @nums;
print join(", ", @sorted);    # 1, 2, 3, 5, 7, 9

# Spaceship operator <=> returns -1, 0, or 1
print 10 <=> 20;    # -1
§06

Arrays

perl
use List::Util qw(sum min max first);

my @colors = ("red", "green", "blue");
my @range  = (1..10);                   # range operator

# Indexing
print $colors[0];     # "red"
print $colors[-1];    # "blue"  (negative index from end)
print scalar @colors; # 3       (element count)
print $#colors;       # 2       (last valid index)

# Modification
push    @colors, "yellow";   # Add to end
my $out = pop @colors;       # Remove from end
unshift @colors, "white";    # Add to front
my $fst = shift @colors;     # Remove from front
splice(@colors, 1, 1, "lime"); # Replace element at index 1

# Functional operations — grep, map, sort
my @evens   = grep { $_ % 2 == 0 } @range;
my @doubled = map  { $_ * 2 }      @range;
my @words   = qw(banana apple cherry date elderberry);
my @bylen   = sort { length($a) <=> length($b) || $a cmp $b } @words;

print "sum=", sum(@range), "  min=", min(@range), "  max=", max(@range), "\n";

# join and split
my $csv   = join(",", @words);
my @back  = split(/,/, $csv);

# wantarray — context detection
sub ctx { return wantarray ? (1, 2, 3) : "scalar_result" }
my @as_list   = ctx();     # (1, 2, 3)
my $as_scalar = ctx();     # "scalar_result"
Sample Output
sum=55 min=1 max=10 Doubles: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 By length: date, apple, banana, cherry, elderberry
§07

Hashes

perl
my %person = (
    name  => "Bob",           # fat comma auto-quotes the left side
    age   => 25,
    city  => "Austin",
    email => 'bob@example.com',
);

# Access, add, delete
my $name  = $person{name};
$person{phone} = "555-1234";
delete $person{phone};

# Test keys
print exists  $person{name} ? "exists"  : "missing";
print defined $person{age}  ? "defined" : "undef";

# Iterate — always sort keys for reproducible output
foreach my $key (sort keys %person) {
    printf "  %-8s => %s\n", $key, $person{$key};
}

# Hash slice — get or set multiple keys at once
my @info            = @person{qw(name city)};
@person{qw(x y)}    = (10, 20);

# Common patterns
my @data = qw(pear apple banana cherry apple pear pear);

# Frequency count
my %freq;
$freq{$_}++ for @data;

# Unique elements preserving order
my %seen;
my @unique = grep { !$seen{$_}++ } @data;
§08–09

Control Flow & Loops

perl
# if / elsif / else
my $score = 78;
if    ($score >= 90) { print "A\n" }
elsif ($score >= 80) { print "B\n" }
elsif ($score >= 70) { print "C\n" }  # → prints "C"
else                 { print "F\n" }

unless ($score < 60) { print "Passing!\n" }  # unless = opposite of if

# Postfix (statement modifier) — readable one-liners
print "Positive\n" if $score > 0;
print "Not zero\n" unless $score == 0;

# Ternary and defined-or
my $result = ($score >= 60) ? "Pass" : "Fail";
my $val    = undef // "default";   # "default"

# Loops
my $i = 1;
while ($i <= 5) { print "$i "; $i++ }       # while
for (my $n=0; $n<5; $n++) { print "$n " }   # C-style for
foreach my $c (qw(red green blue)) { print "$c " }  # foreach
print "$_ " for 1..5;                         # postfix for

# Loop control
for my $n (1..20) {
    next if $n % 2 == 0;    # skip even numbers
    last if $n > 9;          # stop after 9
    print "$n ";             # → 1 3 5 7 9
}

# Loop labels — control outer loop from inner loop
OUTER: for my $row (1..3) {
    for my $col (1..3) {
        next OUTER if $col == 2;
        print "($row,$col) ";
    }
}
# → (1,1) (2,1) (3,1)

# map and grep — functional loops
my @squares = map  { $_ ** 2 } 1..8;
my @odds    = grep { $_ % 2  } 1..10;
§10

Regular Expressions

perl
my $text = "The quick brown fox jumps over the lazy dog.";

# Basic match / negated match
print "has fox\n" if $text =~ /fox/;
print "no cat\n"  if $text !~ /cat/;

# Case-insensitive with /i modifier
print "found\n" if $text =~ /QUICK/i;

# Substitution — s/old/new/flags
(my $copy = $text) =~ s/fox/cat/;        # Replace first
(my $copy2= $text) =~ s/\b\w{4}\b/XXXX/g; # Replace all 4-letter words

# Capturing groups — $1, $2, $3 ...
my $date = "Dates: 2024-05-04 and 2024-12-25.";
if ($date =~ /(\d{4})-(\d{2})-(\d{2})/) {
    print "Year=$1  Month=$2  Day=$3\n";
}

# Find ALL matches with /g in list context
my @all_dates = ($date =~ /(\d{4}-\d{2}-\d{2})/g);
print join(", ", @all_dates);    # 2024-05-04, 2024-12-25

# Named captures — $+{name}
my $log = "ERROR 404: Page not found";
if ($log =~ /(?<level>\w+)\s+(?<code>\d+):\s+(?<msg>.+)/) {
    printf "Level=%-8s Code=%-5s Msg=%s\n",
           $+{level}, $+{code}, $+{msg};
}

# tr — transliteration
(my $upper = "Hello World") =~ tr/a-z/A-Z/;
my $count = ("Hello" =~ tr/aeiouAEIOU//);   # Count vowels → 2

# Split on regex
my @fields = split(/\s*,\s*/, "one,  two ,three");

# Extended /x mode — add comments for readability
my $valid = 'user@example.com' =~ /
    ^           # Start
    [\w.+\-]+   # Local part
    @           # At-sign
    [\w\-]+     # Domain
    (\.\w+)+    # Extensions
    $           # End
/x;
§11

Subroutines

perl
# Arguments arrive in @_ — always unpack with my
sub greet {
    my ($name, $greeting) = @_;
    $greeting //= "Hello";          # default value
    return "$greeting, $name!";
}

# Multiple return values
sub minmax {
    my @s = sort { $a <=> $b } @_;
    return ($s[0], $s[-1]);
}
my ($min, $max) = minmax(5, 2, 8, 1, 9);

# Named parameters via hash
sub create_user {
    my (%args) = @_;
    return { name => $args{name} // "Anon", role => $args{role} // "user" };
}
my $u = create_user(name => "Carol", role => "admin");

# Anonymous sub stored in a scalar
my $square = sub { $_[0] ** 2 };
print $square->(5);    # 25

# Closure — captures $factor from the enclosing scope
sub make_multiplier {
    my ($factor) = @_;
    return sub { $_[0] * $factor };
}
my $double = make_multiplier(2);
my $triple = make_multiplier(3);
print $double->(7);   # 14
print $triple->(7);   # 21

# Dispatch table — hash of code refs
my %ops = (
    add => sub { $_[0] + $_[1] },
    mul => sub { $_[0] * $_[1] },
    div => sub { $_[1] ? $_[0]/$_[1] : "undef" },
);
print $ops{add}->(10, 3);   # 13

# Recursive subroutine
sub factorial { $_[0] <= 1 ? 1 : $_[0] * factorial($_[0]-1) }
printf "%d! = %d\n", $_, factorial($_) for 0..8;
§12

References & Complex Data Structures

perl
use List::Util qw(sum);
use Data::Dumper;

# Anonymous array ref [ ] and hash ref { }
my $aref = [10, 20, 30];
my $href = { x => 1, y => 2 };

print $aref->[1];    # 20
print $href->{x};    # 1

# Array of hashes — most common "record" structure
my @team = (
    { name => "Alice", role => "dev",  level => 5 },
    { name => "Bob",   role => "ops",  level => 3 },
    { name => "Carol", role => "dev",  level => 7 },
);

# Sort, filter, transform via arrow notation
my @by_level = sort { $b->{level} <=> $a->{level} } @team;
my @devs     = grep { $_->{role} eq "dev" } @team;
my $total    = sum(map { $_->{level} } @team);

printf "  %-8s %-8s Lv.%d\n", $_->{name}, $_->{role}, $_->{level}
    for @by_level;

# Hash of arrays — group-by pattern
my %by_role;
push @{ $by_role{$_->{role}} }, $_->{name} for @team;
for my $role (sort keys %by_role) {
    printf "  %-8s: %s\n", $role, join(", ", @{ $by_role{$role} });
}

# Deeply nested structure
my $company = {
    depts => {
        eng => { head => "Alice", staff => [qw(Bob Carol)] },
    },
};
push @{ $company->{depts}{eng}{staff} }, "Dave";
print join(", ", @{ $company->{depts}{eng}{staff} });
# → Bob, Carol, Dave

# Data::Dumper for debugging
local $Data::Dumper::Indent   = 1;
local $Data::Dumper::Sortkeys = 1;
print Dumper({ nums => [1..3], meta => { env => "dev" } });
§13

File I/O

perl
# Three-argument open — always use this form
open(my $wfh, ">", "/tmp/test.txt")  or die "Cannot write: $!";
print $wfh "Hello, file!\n";
printf $wfh "Squared: %d\n", 7**2;
close($wfh);

# Read line by line — memory efficient for large files
open(my $rfh, "<", "/tmp/test.txt") or die "Cannot read: $!";
while (my $line = <$rfh>) {
    chomp $line;       # Remove trailing \n
    print "[$line]\n";
}
close($rfh);

# Slurp entire file into a string at once
open(my $sfh, "<", "/tmp/test.txt") or die "$!";
my $content = do { local $/; <$sfh> };
close($sfh);
printf "File: %d chars\n", length($content);

# File test operators
printf "-e exists:   %s\n", (-e "/tmp/test.txt" ? "yes" : "no");
printf "-f regular:  %s\n", (-f "/tmp/test.txt" ? "yes" : "no");
printf "-s size:     %d bytes\n", -s "/tmp/test.txt";

# Append mode
open(my $app, ">>", "/tmp/test.txt") or die "$!";
print $app "Appended line\n";
close($app);

unlink "/tmp/test.txt";    # Delete the file
§15–16

Error Handling & OOP

perl
# eval — catch exceptions (Perl's try/catch)
eval { die "Something failed!\n" };
print "Caught: $@" if $@;

# Structured error object
eval { die { code => 404, msg => "Not found" } };
if (ref $@ eq 'HASH') {
    printf "Error %d: %s\n", $@->{code}, $@->{msg};
}

# ── Object-Oriented Perl ──────────────────────────────────
{ package Animal;
  sub new {
      my ($class, %a) = @_;
      bless { name => $a{name}//"?", sound => $a{sound}//"..." }, $class;
  }
  sub name  { $_[0]->{name} }
  sub speak { printf "%s says: %s\n", $_[0]->{name}, $_[0]->{sound} }
}
{ package Dog;
  our @ISA = ('Animal');
  sub new { my($cl,%a)=@_; $a{sound}="Woof"; $cl->Animal::new(%a) }
  sub fetch { print "$_[0]->{name} fetches!\n" }
}
{ package Cat;
  our @ISA = ('Animal');
  sub new { my($cl,%a)=@_; $a{sound}="Meow"; $cl->Animal::new(%a) }
  sub purr { print "$_[0]->{name} purrs.\n" }
}
package main;

my $dog = Dog->new(name => "Rex");
my $cat = Cat->new(name => "Whiskers");

$dog->speak;   # Rex says: Woof
$cat->speak;   # Whiskers says: Meow
$dog->fetch;   # Rex fetches!
$cat->purr;    # Whiskers purrs.

# ref() tells you the class; isa() checks the hierarchy
print ref($dog);                # Dog
print $dog->isa("Animal");     # 1 (true — inherited!)
print $dog->isa("Cat");        # "" (false)
Bonus

Mini Address Book — Putting It Together

This final section combines hashes, sorting, filtering, grouping, and formatting into a practical program.

perl
my @contacts = (
    { first => "Alice",  last => "Anderson", city => "Austin",  phone => "555-0101" },
    { first => "Bob",    last => "Brown",    city => "Boston",  phone => "555-0202" },
    { first => "Carol",  last => "Clark",    city => "Austin",  phone => "555-0303" },
    { first => "Dave",   last => "Davis",    city => "Dallas",  phone => "555-0404" },
);

sub full_name { "$_[0]->{first} $_[0]->{last}" }

# Print all, sorted by last name
printf "  %-20s  %-12s  %s\n", full_name($_), $_->{phone}, $_->{city}
    for sort { $a->{last} cmp $b->{last} } @contacts;

# Filter — only Austin contacts
my @austin = grep { $_->{city} eq "Austin" } @contacts;
print full_name($_), "\n" for @austin;

# Group by city
my %by_city;
push @{ $by_city{$_->{city}} }, full_name($_) for @contacts;
printf "  %-10s: %s\n", $_, join(", ", sort @{ $by_city{$_} })
    for sort keys %by_city;

# Search by last name
my ($found) = grep { $_->{last} =~ /^Davis$/i } @contacts;
printf "Found: %s (%s)\n", full_name($found), $found->{phone} if $found;

# CSV export
print "First,Last,Phone,City\n";
printf "%s,%s,%s,%s\n", @{$_}{qw(first last phone city)}
    for sort { $a->{last} cmp $b->{last} } @contacts;
Output
Alice Anderson 555-0101 Austin Bob Brown 555-0202 Boston Carol Clark 555-0303 Austin Dave Davis 555-0404 Dallas Alice Anderson Carol Clark Austin : Alice Anderson, Carol Clark Boston : Bob Brown Dallas : Dave Davis Found: Dave Davis (555-0404) First,Last,Phone,City Alice,Anderson,555-0101,Austin ...
Next step: Run perl perl_tutorial.pl to see all output live in your terminal, then open Tutorial 02 — GUI Basics to start building windows.