Advanced Perl  ·  Level 3  ·  Deep Explanation Series

Advanced Perl
with OOP

Meta-programming, operator overloading, roles, type systems, concurrency, and the full object-oriented model — all explained line-by-line.

14Concepts
OOPFocus
5Sections
L3Advanced
OOP Foundations
01
bless, packages & the object model
How Perl OOP really works under the hood
bless packages OOP core

Perl's object system has exactly three moving parts: references (for data), packages (for namespaces and method tables), and bless (which links a reference to a package). That's all there is. A Perl "object" is simply a reference that has been told which package to look up its methods in.

Understanding this deeply matters because every OOP framework (Moose, Moo, Class::Accessor) is built on these three primitives. When things go wrong, you debug at this level.

bless_deep.pl
package Animal;
use strict; use warnings;

sub new {
    my ($class, %args) = @_;

    # bless takes: a REFERENCE, and a class name (string)
    # It stamps the reference so Perl knows where to look for methods
    my $self = bless {
        name  => $args{name} // 'Unknown',
        sound => $args{sound} // '...',
    }, $class;      # $class NOT 'Animal' — allows subclassing

    return $self;
}

# Methods: just subs where $_[0] (or $self) is the invocant
sub name  { my $self = shift; return $self->{name}  }
sub sound { my $self = shift; return $self->{sound} }
sub speak {
    my $self = shift;
    printf "%s says: %s\n", $self->name, $self->sound;
}

# Class method — called on the package, not an instance
sub kingdom { return 'Animalia' }

package main;

my $a = Animal->new(name => 'Parrot', sound => 'Squawk');
$a->speak();

# Peek under the hood
print ref($a);          # "Animal" — the blessing class
print $a->isa('Animal'); # 1
print $a->can('speak');  # returns the coderef for speak()

# Re-bless: change an object's class at runtime
bless $a, 'Dog';          # $a is now a Dog (if Dog package exists)
1
bless $ref, $class modifies the reference in-place, stamping it with the class name. It returns the same reference. The stamp is stored in the reference itself — you can inspect it with ref($obj).
2
Use $class not 'Animal' in constructors. When a subclass calls Animal->new(), $class is the subclass name. Hard-coding 'Animal' breaks inheritance — all instances would be blessed as Animal even when created via a subclass.
3
Method dispatch via ->: $a->speak() is syntactic sugar for Animal::speak($a). Perl looks up the method in ref($a)'s package (or its @ISA chain), then calls it with $a as the first argument.
4
can() returns the coderef for a method if the object can call it, or undef. Use this instead of eval { $obj->method } to check capability. It's also how duck-typing works in Perl.
02
Inheritance with @ISA and SUPER
The inheritance chain, method lookup, and cooperative overriding
inheritance @ISA SUPER

Perl inheritance is implemented through the @ISA array in a package. When Perl can't find a method in the current package, it searches the packages listed in @ISA, recursively. use parent is the modern way to set @ISA.

SUPER:: calls the parent's version of the current method — essential for cooperative inheritance where a child extends rather than replaces parent behaviour. The tricky part is that SUPER:: is resolved at compile time relative to the package it's written in, not the object's class.

inheritance.pl
package Animal;
sub new   { my ($c,%a)=@_; bless{name=>$a{name}},$c }
sub name  { $_[0]->{name} }
sub describe {
    my $self = shift;
    return "Animal: " . $self->name;
}

package Dog;
use parent -norequire, 'Animal';  # sets @Dog::ISA = ('Animal')
                                     # -norequire: don't load Animal.pm

sub new {
    my ($class, %args) = @_;
    # Call parent constructor, then add Dog-specific data
    my $self = $class->SUPER::new(%args);
    $self->{breed} = $args{breed} // 'Mixed';
    return $self;
}

sub describe {
    my $self = shift;
    # SUPER:: calls Animal::describe — extend, don't replace
    my $base = $self->SUPER::describe();
    return $base . " (Dog, breed: " . $self->{breed} . ")";
}

package GoldenRetriever;
use parent -norequire, 'Dog';

sub describe {
    my $self = shift;
    return $self->SUPER::describe() . " [Golden!]";
}

package main;
my $g = GoldenRetriever->new(name=>'Buddy', breed=>'Golden');
print $g->describe();
# Animal: Buddy (Dog, breed: Golden) [Golden!]

# Inspect the ISA chain
print join(' -> ', $g->isa($_) ? $_ : ()
    for qw(GoldenRetriever Dog Animal));
1
use parent -norequire, 'Animal': without -norequire, Perl tries to require Animal from disk. Use -norequire when both classes are in the same file (common in single-file examples and tests).
2
$class->SUPER::new(%args): calling SUPER::new on $class (not $self) is critical in constructors. If you wrote $self->SUPER::new, you'd be calling the parent constructor on an already-constructed object. Always use the class variable for super constructor calls.
3
Cooperative inheritance: each class calls SUPER::describe() and appends to the result, creating a layered string. This is the proper pattern — each class is responsible for its own additions without duplicating parent logic.
4
MRO (Method Resolution Order): by default Perl uses DFS (depth-first search) for @ISA. For diamond inheritance, use use mro 'c3' which implements the same linearization algorithm as Python 3. SUPER:: always follows the MRO.
03
Method Resolution Order & UNIVERSAL
DFS vs C3, AUTOLOAD, DESTROY, and the UNIVERSAL base class
MRO AUTOLOAD UNIVERSAL

AUTOLOAD is a special sub that Perl calls when a method is not found anywhere in the ISA chain. It receives the fully-qualified method name in $AUTOLOAD. This is how many accessor-generation modules work — they intercept unknown method calls and create accessors on the fly. DESTROY is called automatically when an object's reference count drops to zero — Perl's destructor.

UNIVERSAL is the invisible base class of every Perl object. It provides isa(), can(), and DOES() — available on every object without any inheritance declaration.

autoload_destroy.pl
package AutoAccessor;
use strict; use warnings;

# Our AUTOLOAD uses 'our' because $AUTOLOAD is a package global
our $AUTOLOAD;

sub new {
    my ($class, %data) = @_;
    return bless \%data, $class;
}

sub AUTOLOAD {
    my $self = shift;

    # $AUTOLOAD is "Package::method_name" — strip the package prefix
    my $method = $AUTOLOAD;
    $method =~ s/.*:://;   # remove "AutoAccessor::"

    # CRUCIAL: don't intercept DESTROY — it would suppress warnings
    return if $method eq 'DESTROY';

    # Act as a getter/setter for any hash key
    if (exists $self->{$method}) {
        # Install the method permanently so AUTOLOAD isn't called again
        no strict 'refs';
        *{"AutoAccessor::$method"} = sub {
            my $s = shift;
            $s->{$method} = shift if @_;  # setter if arg given
            return $s->{$method};
        };
        return $self->{$method};
    }
    die "No such attribute: $method\n";
}

sub DESTROY {
    my $self = shift;
    print "Destroying: " . $self->{name} . "\n";
}

package main;
my $obj = AutoAccessor->new(name=>'Foo', value=>42);
print $obj->name();    # triggers AUTOLOAD -> installs name()
print $obj->name();    # calls installed name() directly — AUTOLOAD skipped
$obj->value(99);       # setter: sets $self->{value} = 99
1
Always return early for DESTROY. When an object goes out of scope, Perl calls DESTROY. If your AUTOLOAD intercepts it (because you didn't return early), it suppresses the automatic destructor logic and can cause memory leaks and spurious warnings.
2
Install the method permanently using the glob trick *{"Package::method"} = sub { ... }. This stores the coderef in the package's symbol table. The next call goes directly to the installed method — AUTOLOAD is not invoked again. This is called method caching.
3
no strict 'refs' is needed to use a variable as a glob name like *{"Package::$method"}. Strict mode normally forbids symbolic references (using a string as a variable name). This block-scoped pragma disables that check for just the next statement.
4
$AUTOLOAD contains the full name including the package: "AutoAccessor::name". Always strip the package prefix with s/.*:://. Otherwise your attribute lookup would search for "AutoAccessor::name" in the hash instead of "name".
04
Operator Overloading
use overload — making objects behave like built-in types
overload operators

The overload pragma lets you define what happens when standard Perl operators are applied to objects. This is how you make a Vector class support $v1 + $v2, or a BigNum class support $n > 100, or a custom class print meaningfully when interpolated in a string.

overload_vector.pl
package Vector;
use strict; use warnings;
use overload
    '+'   => \&add,
    '-'   => \&subtract,
    '*'   => \&scale,
    '""'  => \&stringify,   # called when object is used as string
    '=='  => \&equal,
    'abs' => \&magnitude,
    'neg' => sub { Vector->new(-$_[0]->{x}, -$_[0]->{y}) };

sub new {
    my ($class, $x, $y) = @_;
    return bless { x => $x, y => $y }, $class;
}

# Operator handlers receive: ($left, $right, $swap)
# $swap is true if the operands were reversed (e.g. 3 + $vec vs $vec + 3)
sub add {
    my ($a, $b) = @_;
    return Vector->new($a->{x}+$b->{x}, $a->{y}+$b->{y});
}
sub subtract {
    my ($a, $b, $swap) = @_;
    return $swap
        ? Vector->new($b->{x}-$a->{x}, $b->{y}-$a->{y})
        : Vector->new($a->{x}-$b->{x}, $a->{y}-$b->{y});
}
sub scale     { Vector->new($_[0]->{x}*$_[1], $_[0]->{y}*$_[1]) }
sub magnitude { sqrt($_[0]->{x}**2 + $_[0]->{y}**2) }
sub stringify { "(" . $_[0]->{x} . "," . $_[0]->{y} . ")" }
sub equal     { $_[0]->{x}==$_[1]->{x} && $_[0]->{y}==$_[1]->{y} }

package main;
my ($v1, $v2) = (Vector->new(3,4), Vector->new(1,2));
print $v1 + $v2;     # "(4,6)"  — calls add(), then stringify()
print $v1 * 3;       # "(9,12)" — calls scale()
print abs($v1);     # "5"      — calls magnitude(): sqrt(9+16)
1
The three arguments to operator handlers: ($left, $right, $swap). $swap is 1 when Perl reversed the operands (e.g. 5 - $vec means $left=$vec, $right=5, $swap=1). Always handle $swap for non-commutative operators like -, /, and **.
2
"" (stringify) overload is the most important. It's called whenever Perl needs to convert your object to a string: in print, string interpolation, concatenation, warn, and error messages. Without it, objects print as Vector=HASH(0x...).
3
Overload inheritance: operators are inherited. If ColorVector extends Vector without its own overloads, it uses Vector's. Return ref($a)->new(...) instead of Vector->new(...) in handlers to respect subclasses.
4
Other powerful overloads: 0+ (numification — used in numeric context), bool (truthiness), @{}/%{}/${} (dereference as array/hash/scalar), <=>/cmp (for sort), x (repetition).
Moose Deep Dive
05
Moose Type Constraints & Coercions
Custom types, subtypes, coerce, and type unions
Moose types coerce

Moose's type system goes far beyond the built-in types like Str and Int. You can create subtypes with custom validation constraints, coercions that automatically convert one type to another, and type unions. This is how you enforce business rules at the object-construction level rather than inside methods.

moose_types.pl
package MyTypes;
use Moose::Util::TypeConstraints;

# Subtype: a Str that must match a pattern
subtype 'PositiveInt',
    as      'Int',
    where   { $_ > 0 },
    message { "$_ is not a positive integer" };

subtype 'EmailAddress',
    as      'Str',
    where   { $_ =~ /^[\w.+-]+\@[\w-]+\.\w{2,}$/ },
    message { "'$_' is not a valid email" };

# Coercion: automatically convert ArrayRef to comma-joined Str
coerce 'Str',
    from 'ArrayRef',
    via  { join(', ', @$_) };

package Person;
use Moose;
MyTypes->import();

has 'name' => (
    is     => 'ro',
    isa    => 'Str',
    coerce => 1,         # enable coercion for this attribute
);
has 'age' => (
    is  => 'rw',
    isa => 'PositiveInt',  # custom subtype — dies if <= 0
);
has 'email' => (
    is        => 'rw',
    isa       => 'EmailAddress',
    predicate => 'has_email',  # generates: $p->has_email() -> bool
    clearer   => 'clear_email', # generates: $p->clear_email()
);

package main;
my $p = Person->new(
    name  => ['John', 'Doe'],  # ArrayRef coerced to "John, Doe"
    age   => 30,
    email => 'john@example.com',
);

# This would throw: "0 is not a positive integer"
# Person->new(name => 'X', age => 0);
1
subtype ... as ... where ... message: the as parent determines what's checked first. Only values that pass the parent constraint reach the where block. The message block receives the failing value in $_ and provides a custom error string.
2
Coercions are opt-in per-attribute with coerce => 1. Without that, even if a coercion is defined globally, the attribute won't use it. This is intentional — it keeps behaviour explicit and predictable.
3
predicate and clearer generate helper methods for optional attributes. predicate => 'has_email' creates $obj->has_email() which returns true if the attribute is defined. clearer resets it to undef.
4
Type unions: isa => 'Int | Str' accepts either. You can also use Maybe[Str] (Str or undef), ArrayRef[Int] (arrayref containing only integers), HashRef[Str], and nested parameterized types.
06
Roles, Composition & Method Modifiers
before/after/around, requires, conflicts, and multi-role composition
roles composition modifiers

Roles are Moose's answer to multiple inheritance. Where multiple inheritance creates fragile class hierarchies, roles compose cleanly — conflicts are detected at compile time. The around modifier is the most powerful: it receives the original method as a coderef and can completely control if and how it runs, enabling AOP-style patterns like logging, caching, and timing.

roles_advanced.pl
package Role::Printable;
use Moose::Role;
requires 'to_string';   # consuming class must implement this
sub print_self { print $_[0]->to_string() . "\n" }

package Role::Auditable;
use Moose::Role;
has '_log' => (is=>'rw', isa=>'ArrayRef', default=>sub{[]});
sub log_event {
    my ($self, $event) = @_;
    push @{$self->_log}, sprintf("[%s] %s", scalar localtime, $event);
}
sub audit_trail { return @{$_[0]->_log} }

package BankAccount;
use Moose;
with 'Role::Printable', 'Role::Auditable';  # compose both roles

has 'balance' => (is=>'rw', isa=>'Num', default=>0);

sub deposit {
    my ($self, $amount) = @_;
    $self->balance($self->balance + $amount);
}
sub to_string { "Balance: \$" . $_[0]->balance }

# 'around' wraps deposit: logs before AND after the real method
around 'deposit' => sub {
    my ($orig, $self, $amount) = @_;  # $orig is the original deposit()
    $self->log_event("Before deposit: $amount");
    my $result = $self->$orig($amount);  # call the REAL method
    $self->log_event("After deposit: balance=" . $self->balance);
    return $result;
};

package main;
my $acct = BankAccount->new();
$acct->deposit(100);
$acct->print_self();    # from Role::Printable
print $_, "\n" for $acct->audit_trail();  # from Role::Auditable
1
around receives ($orig, $self, @args). The first argument is the original method as a coderef. Call it as $self->$orig(@args). You can inspect arguments before passing them, modify the return value, or skip calling $orig entirely (caching).
2
Role conflict detection: if two roles both provide a method with the same name, Moose throws a compile-time error. You must resolve it explicitly in the class using -alias or -excludes in the with statement, or by providing your own implementation.
3
Roles can have attributes. The _log attribute in Role::Auditable is composed into any consuming class. The attribute's storage is in the class instance — not shared between instances or in the role itself.
4
before vs after vs around: before can abort (if it dies), but can't change the return value. after sees the result but can't change it. around has full control — use it for caching, input validation, or conditional execution.
07
Moose Meta-programming
Introspecting and modifying classes at runtime via the meta-object protocol
meta MOP introspection

Every Moose class has a metaclass — an object that represents the class itself. Through $class->meta you can inspect and modify the class: list attributes, add methods, find superclasses, and even create new classes entirely at runtime. This is the Meta-Object Protocol (MOP).

moose_meta.pl
package Point;
use Moose;
has $_ => (is=>'rw', isa=>'Num', default=>0) for qw(x y);
sub to_string { "(".$_[0]->x.",".$_[0]->y.")" }

package main;

# $meta is the metaclass object for Point
my $meta = Point->meta;

# Introspect: list all attribute names
print join(', ', sort $meta->get_attribute_list);  # x, y

# Introspect: get details about a specific attribute
my $x_attr = $meta->get_attribute('x');
print $x_attr->type_constraint->name;  # "Num"
print $x_attr->is_required;           # "" (false, has default)

# Add a method to an existing class at runtime
$meta->add_method('magnitude' => sub {
    my $self = shift;
    return sqrt($self->x**2 + $self->y**2);
});

my $p = Point->new(x=>3, y=>4);
print $p->magnitude;   # 5 — method added at runtime works immediately

# Create a new class entirely at runtime via metaclass
my $dynamic_class = Moose::Meta::Class->create(
    'DynamicPoint3D',
    superclasses => ['Point'],
    attributes   => [
        Moose::Meta::Attribute->new('z' => (is=>'rw',isa=>'Num',default=>0))
    ],
);

my $p3 = $dynamic_class->new_object(x=>1,y=>2,z=>3);
print $p3->z;   # 3
1
->meta returns the metaclass instance. In Moose, every class automatically gets a meta() method that returns a Moose::Meta::Class object. This object has methods for everything you can do declaratively (add attributes, methods, roles) plus inspection capabilities.
2
add_method modifies a live class. New instances (and existing instances) gain the method immediately. This is legitimate for building framework code, test helpers, or plugins — but use it carefully in production as it bypasses Moose's declaration machinery.
3
Moose::Meta::Class->create builds an entire class programmatically. This is how ORMs and other meta-frameworks work — they inspect a database schema and generate classes with the right attributes on the fly without any has declarations in source code.
4
Always call __PACKAGE__->meta->make_immutable at the end of a production Moose class. It compiles optimised accessor methods and freezes the class structure, providing a significant performance boost.
Advanced Patterns
08
Inside-Out Objects
Encapsulation without hash exposure — using scalar refs and external storage
inside-out encapsulation

Classic Perl objects store data in a hashref, and any code with the object can access $self->{name} directly — bypassing accessors and violating encapsulation. Inside-out objects store data in hashes keyed by the object's memory address inside the class, not in the object itself. The object is just an opaque scalar reference.

This is a technique you'll encounter in legacy code and libraries like Class::InsideOut. Understanding it deepens your knowledge of Perl's reference counting and memory model.

inside_out.pl
package SecureAccount;
use strict; use warnings;
use Scalar::Util qw(refaddr weaken);

# Data stored OUTSIDE the object, keyed by memory address
# %_balance is private to this package — callers cannot access it
my %_balance;
my %_owner;

sub new {
    my ($class, %args) = @_;

    # The object is a blessed SCALAR ref (not a hash ref)
    my $self = bless \(my $dummy), $class;

    # refaddr returns the numeric memory address — unique per object
    my $id = refaddr($self);

    # Store data in package-level hashes indexed by memory address
    $_balance{$id} = $args{balance} // 0;
    $_owner{$id}   = $args{owner};

    return $self;
}

# Accessors use refaddr to find the right data slot
sub balance { $_balance{refaddr($_[0])} }
sub owner   { $_owner{  refaddr($_[0])} }

sub deposit {
    my ($self, $amt) = @_;
    $_balance{refaddr($self)} += $amt;
}

# DESTROY is MANDATORY — must clean up the external hash entries
sub DESTROY {
    my $id = refaddr($_[0]);
    delete $_balance{$id};
    delete $_owner{$id};   # memory leak if you forget this!
}

package main;
my $acct = SecureAccount->new(owner=>'Alice', balance=>1000);
$acct->deposit(500);
printf "%s: \$%d\n", $acct->owner, $acct->balance;  # Alice: $1500
# print $acct->{balance}; # ERROR: $acct is not a hashref!
1
refaddr($ref) from Scalar::Util returns the numeric memory address of a reference. This is unique for each live object. It's the key insight: use the address as a hash key to look up that object's data in package-level storage.
2
The object is a blessed scalar ref, not a hashref. bless \(my $dummy), $class creates a reference to an anonymous scalar. Callers get back an opaque blessed reference — they literally cannot access the data without going through the accessors.
3
DESTROY is mandatory with inside-out objects. When the object is destroyed, its slot in %_balance and %_owner is not automatically freed — those are separate lexical hashes. Forgetting DESTROY creates a memory leak: the data grows without bound.
4
Memory address reuse is the hidden danger. Perl reuses freed memory addresses. If object A is destroyed and then object B is created at the same address, and you forgot to delete from the hash in DESTROY, object B would inherit A's stale data. Always clean up in DESTROY.
09
Weak References & Circular Structures
Scalar::Util weaken(), reference counting, and avoiding memory leaks
memory weaken Scalar::Util

Perl uses reference counting for garbage collection. When two objects hold references to each other (a circular reference), their counts never reach zero — they leak forever. Scalar::Util::weaken() marks a reference as "weak": it doesn't count toward reference counting and becomes undef automatically when the referent is destroyed. This is how parent-child relationships are safely modelled.

weak_refs.pl
use strict; use warnings;
use Scalar::Util qw(weaken isweak refaddr);

package Node;

sub new {
    my ($class, $name) = @_;
    return bless { name => $name, parent => undef, children => [] }, $class;
}

sub add_child {
    my ($self, $child) = @_;
    push @{$self->{children}}, $child;

    # child holds a reference BACK to parent — this would be circular
    # WITHOUT weaken, neither would ever be garbage-collected
    $child->{parent} = $self;
    weaken($child->{parent});   # make the back-reference WEAK
    # Now: parent's refcount is NOT incremented by this assignment
}

sub DESTROY {
    print "Destroying: $_[0]->{name}\n";
}

package main;

{  # inner scope — objects destroyed when scope ends
    my $root  = Node->new('root');
    my $child = Node->new('child');
    $root->add_child($child);

    print isweak($child->{parent})
        ? "parent ref is weak\n"
        : "parent ref is strong\n";

    # When $root leaves scope, it's destroyed because child's
    # weak parent reference doesn't hold a strong ref to it
}  # prints "Destroying: child" then "Destroying: root"

# If we had NOT used weaken(), neither would ever be destroyed
# because: root keeps child alive, child keeps root alive — cycle
1
Reference counting works by incrementing a counter each time you copy a reference, and decrementing when it goes out of scope. When the count hits zero, memory is freed. A circular reference means A holds B (count: 1) and B holds A (count: 1) — even when your code has no references, both counts are still 1.
2
weaken($ref) modifies the reference in-place. After weakening, the reference still points to the object and works normally. But it no longer increments the reference count. When all strong references to the object are gone, it's destroyed, and the weak reference becomes undef.
3
Always weaken the "back" pointer. In parent-child relationships, the parent holds a strong reference to each child (keeping children alive). Children hold a weak reference back to the parent. This way, the parent's lifetime controls the tree — destroy the root and everything cleans up.
4
Check with isweak($ref) to verify. Also, Scalar::Util::blessed($ref) returns the class name of a blessed ref (like ref() but returns undef for non-objects instead of the ref type). Use blessed for type checking in library code.
10
The Tie Interface
Overloading built-in variable behaviour — scalars, arrays, hashes, filehandles
tie magic

tie lets you attach a class to a regular Perl variable — scalar, array, hash, or filehandle — so that ordinary operations like $x = 5, push @a, 1, or $h{key} trigger your class's methods. This is "transparent magic" — code using the tied variable doesn't know anything special is happening.

Classic uses: variables that persist to disk on assignment, hashes that enforce unique values, read-only scalars, logging variables, and environment-variable hash overlays.

tie_example.pl
package TiedScalar;
# Must implement TIESCALAR, FETCH, STORE, DESTROY for a tied scalar

sub TIESCALAR {         # called when tie() is invoked
    my ($class, %opts) = @_;
    return bless {
        value   => $opts{default},
        min     => $opts{min},
        max     => $opts{max},
        history => [],
    }, $class;
}

sub FETCH {            # called when reading $scalar
    return $_[0]->{value};
}

sub STORE {            # called when writing $scalar = value
    my ($self, $new_val) = @_;

    # Range validation on every assignment
    if (defined $self->{min} && $new_val < $self->{min}) {
        die "Value $new_val below minimum $self->{min}\n";
    }
    if (defined $self->{max} && $new_val > $self->{max}) {
        die "Value $new_val above maximum $self->{max}\n";
    }
    push @{$self->{history}}, $self->{value};
    $self->{value} = $new_val;
}

sub DESTROY {}

package main;
use strict; use warnings;

# tie $scalar, 'ClassName', constructor_args...
tie my $temp, 'TiedScalar',
    default => 20, min => -273, max => 1000;

print $temp;     # 20  — triggers FETCH
$temp = 37;     # triggers STORE, logs 20 to history
$temp = 100;    # triggers STORE
# $temp = 2000; # dies: "Value 2000 above maximum 1000"

# Access the underlying tied object to inspect history
my $obj = tied $temp;
print join(', ', @{$obj->{history}});  # "20, 37"

# untie removes the magic, returns to normal variable
untie $temp;
1
The required interface for tied scalars: TIESCALAR (constructor), FETCH (read), STORE (write), DESTROY. For tied arrays: TIEARRAY, FETCH, STORE, FETCHSIZE, STORESIZE, PUSH, POP, etc. For tied hashes: TIEHASH, FETCH, STORE, EXISTS, DELETE, FIRSTKEY, NEXTKEY.
2
tied $var returns the underlying tied object. This is the only way to access the implementation object directly — calling its private methods or inspecting internal state. Without this, the tie is truly transparent.
3
Performance cost: tied variables are significantly slower than plain variables because every access goes through a method call. Prefer tie for situations where the magic justifies the overhead — persistent storage, validation, debugging — not hot paths.
4
Real-world uses: Tie::File (array tied to a file), Tie::IxHash (hash that preserves insertion order), DB_File/GDBM_File (hashes persisted to disk files), and Readonly (makes variables read-only by tying them to a class that dies on STORE).
Systems Programming
11
XS & Inline::C
Calling C code from Perl for performance-critical operations
XS C extension performance

When pure Perl isn't fast enough, XS (eXternal Subroutines) is the standard way to write Perl extension modules in C. Inline::C is a friendlier alternative that compiles C code embedded directly in a Perl file — ideal for learning and one-off optimisation without a full module structure.

The key concepts are: the Perl C API (SV, AV, HV), the argument stack, and the XS typemap that converts between Perl scalars and C types.

inline_c.pl
use strict;
use warnings;
use Inline C => <<'END_C';

/* C code compiled and linked at runtime by Inline::C */

/* Fast integer GCD using Euclidean algorithm in C */
long fast_gcd(long a, long b) {
    while (b) {
        long t = b;
        b = a % b;
        a = t;
    }
    return a;
}

/* Sieve of Eratosthenes — returns count of primes up to n */
int count_primes(int n) {
    char *sieve = (char *)calloc(n + 1, sizeof(char));
    int count = 0;
    for (int i = 2; i <= n; i++) {
        if (!sieve[i]) {
            count++;
            for (int j = i*2; j <= n; j += i)
                sieve[j] = 1;
        }
    }
    free(sieve);
    return count;
}

END_C

# Perl calls C functions as if they were Perl subs
printf "GCD(48, 36) = %d\n", fast_gcd(48, 36);       # 12
printf "Primes up to 1M: %d\n", count_primes(1_000_000);  # 78498

# XS typemap handles the Perl-C type conversion automatically:
# Perl integer (SvIV) <-> C int/long
# Perl float (SvNV)   <-> C double
# Perl string (SvPV)  <-> C char*
1
use Inline C => '...' compiles the C code on first run using the system C compiler, caches the result in _Inline/, and links it into the running Perl process. Subsequent runs use the cache. The compilation only happens when the C code changes.
2
Typemap conversion is automatic for basic types. Inline::C (via XS) reads the C function signature and generates the glue code: Perl's SV* (scalar value) is converted to int/long/double/char* going in, and back to a Perl scalar on return.
3
Full XS is more powerful but complex. A proper XS module has a .xs file (C with XS directives), a typemap file for type conversions, and a Makefile.PL. Use h2xs or Module::Build to scaffold it. Inline::C is perfect for prototyping before moving to full XS.
4
When to reach for XS/Inline::C: tight loops processing millions of items, bit-manipulation, interfacing with C libraries, crypto primitives, or any computation where a profiler shows Perl overhead is the bottleneck. Often a well-written Perl solution with the right CPAN module (which is already in C) is faster to deploy.
12
Concurrency: forks, threads & async
fork(), Parallel::ForkManager, threads, and IO::Async patterns
concurrency fork async

Perl has three concurrency models: fork-based (separate processes, no shared memory), threads (shared memory, complex synchronisation), and event-loop/async (single-threaded, non-blocking I/O via IO::Async or AnyEvent). Fork is the most idiomatic and safest for CPU-bound work. Threads in Perl are expensive and have significant caveats.

parallel_fork.pl
use strict;
use warnings;
use Parallel::ForkManager;  # CPAN — manages a pool of child processes
use POSIX qw(WNOHANG);

# Process 12 URLs with max 4 parallel workers
my @urls    = map { "https://api.example.com/item/$_" } 1..12;
my %results;

my $pm = Parallel::ForkManager->new(4);

# Callback: called in PARENT process when a child finishes
# $data_structure is what the child passed to finish()
$pm->run_on_finish(sub {
    my ($pid, $exit, $ident, $signal, $core, $data) = @_;
    if (defined $data) {
        %results = (%results, %$data);   # merge child's result
    }
});

for my $url (@urls) {
    my $pid = $pm->start($url) and next;

    # --- CHILD PROCESS CODE ---
    # This block runs in a forked child. No shared memory with parent.
    my $data = fetch_url($url);    # hypothetical HTTP fetch

    # Pass result back to parent via serialised data structure
    $pm->finish(0, { $url => $data });
    # --- END CHILD CODE ---
}

$pm->wait_all_children;   # blocks until all children complete

# Raw fork() pattern (lower level):
my $pid = fork();
die "fork failed: $!" unless defined $pid;
if ($pid == 0) {
    # CHILD: $pid == 0 in child process
    print "Child PID: $$\n";
    exit 0;
} else {
    # PARENT: $pid is the child's PID
    waitpid($pid, 0);   # wait for specific child
}
1
1
$pm->start($ident) and next — the idiomatic Parallel::ForkManager loop. start() forks a child. In the parent, it returns the child's PID (a true value), so and next skips the rest of the loop body. In the child, it returns 0 (false), so the child proceeds into the work block.
2
Fork creates a complete copy of the process. All variables are copied (copy-on-write). Changes in the child do NOT affect the parent — you must pass results back explicitly. Parallel::ForkManager serializes the hashref with Storable and passes it via a pipe.
3
In raw fork(): $pid == 0 in the child, $pid > 0 (child's PID) in the parent, undef on failure. Always die on undef. Always call waitpid or wait in the parent — otherwise children become zombies, consuming process table entries.
4
Perl threads (use threads) vs fork: threads share memory but require locks (threads::shared, Thread::Semaphore). They're heavy (Perl duplicates the interpreter per thread) and many CPAN modules aren't thread-safe. For most tasks, fork is simpler and more reliable.
Perl Internals & Meta
13
Symbol Tables & Glob Manipulation
%main::, typeglobs, dynamic method installation, import mechanics
symbol table globs meta

Every Perl package has a symbol table — a hash stored as %PackageName:: where each key is a symbol name and each value is a typeglob (*symbol). A typeglob is a container that simultaneously holds up to six "slots": scalar, array, hash, code, filehandle, and format. This is how Exporter works — it copies coderefs from one package's symbol table into another.

symbol_tables.pl
use strict;
use warnings;

# --- Inspect a package's symbol table ---
package MyLib;
sub hello { "hello" }
sub world { "world" }
our $VERSION = '1.0';

package main;

# %MyLib:: is the symbol table hash
for my $sym (sort keys %MyLib::) {
    my $code = *{"MyLib::$sym"}{CODE};   # extract CODE slot from glob
    print "$sym => " . (defined $code ? "sub" : "var") . "\n";
}

# --- Install a method at runtime (used by import, Exporter, etc.) ---
{
    no strict 'refs';    # required for symbolic glob assignment

    # Assign to the CODE slot of the glob — adds/replaces a sub
    *{"main::greet"} = sub { "Hello, $_[0]!" };
}
print greet("World");   # "Hello, World!" — installed method works

# --- How Exporter works (simplified) ---
sub my_import {
    my ($from_pkg, $to_pkg, @funcs) = @_;
    no strict 'refs';
    for my $fn (@funcs) {
        # Copy the CODE glob slot from source to destination package
        *{"${to_pkg}::${fn}"} = \&{"${from_pkg}::${fn}"};
    }
}

my_import('MyLib', 'main', 'hello', 'world');
print hello() . " " . world();   # "hello world"
1
The symbol table %MyLib:: is a real hash you can iterate over. Keys are symbol names; values are typeglobs. Note the double colon at the end of the package name.
2
Glob slots: *{"Pkg::name"}{CODE}, {SCALAR}, {ARRAY}, {HASH}, {IO}, {FORMAT}. You can install only a specific slot without touching the others. Assigning a coderef to the CODE slot (*foo = \&bar) creates an alias, not a copy.
3
\&{"PackageName::subname"} takes a reference to a named function using a string. This requires no strict 'refs'. It's how Exporter copies functions between packages and how many AUTOLOAD implementations install methods permanently.
4
The import() mechanism: when you write use Foo qw(bar), Perl calls Foo::import('Foo', 'bar'). Exporter-based modules implement import() as the glob-copying function above. This is the entire magic behind all use Module qw(...) imports.
14
BEGIN, END & Perl's Compilation Phases
Compile time vs runtime, CHECK, INIT, UNITCHECK, and source filters
BEGIN phases compile-time

Perl programs don't simply run top-to-bottom. There are distinct compilation phases. Understanding them is essential for advanced metaprogramming, writing pragmas, and debugging mysterious "use constant" or "use strict" failures. BEGIN runs immediately when compiled (before anything else in the file executes). END runs after the program exits. CHECK, INIT, and UNITCHECK run at different transition points.

Perl Execution Phases
Source loaded BEGIN runs Compile rest UNITCHECK CHECK INIT Runtime runs END
phases.pl
use strict;
use warnings;

# BEGIN runs as SOON as it is compiled — before the rest of the file
BEGIN {
    print "1. BEGIN — compile time\n";

    # use statements are compiled to: BEGIN { require M; M->import() }
    # That's why constants from 'use constant' are available immediately
}

# Multiple BEGINs run in order of appearance
BEGIN { print "2. second BEGIN\n" }

# UNITCHECK: after the current unit (file or eval) is compiled
UNITCHECK { print "3. UNITCHECK\n" }

# CHECK: after ALL compilation is done, before runtime
CHECK     { print "4. CHECK\n" }

# INIT: first thing at runtime
INIT      { print "5. INIT — start of runtime\n" }

print "6. Runtime code\n";

# END: runs when the program exits (even on die), LIFO order
END       { print "7. END\n" }

# Practical BEGIN use: conditionally load modules based on Perl version
BEGIN {
    if ($] >= 5.020) {
        require feature;
        feature->import('say', 'state');
    }
}

# use constant is really: BEGIN { *NAME = \$value } in the symbol table
use constant {
    MAX_RETRIES => 3,
    TIMEOUT     => 30,
};
# Perl inlines constant values at compile time — no hash lookup at runtime

# END cleanup: always runs, even after die or exit()
END {
    unlink '/tmp/my_lockfile';   # guaranteed cleanup
}
1
1
BEGIN runs at compile time, immediately. This means it runs even under perl -c (syntax check only). That's why use Foo is safe to put anywhere — it compiles to BEGIN { require Foo; Foo->import() } and the module is loaded before any runtime code.
2
Multiple BEGIN blocks run in order, but all run before any runtime code, no matter where in the file they appear. This surprises beginners: a BEGIN at line 100 runs before a bare print at line 5.
3
use constant is compile-time magic. It installs a constant sub in the symbol table during BEGIN. Perl's optimizer inlines constant values — MAX_RETRIES is replaced with 3 at compile time, making it as fast as a literal number with zero runtime lookup cost.
4
END blocks are a safety net for cleanup. They run after exit(), after uncaught die(), and even after signals (if you have a signal handler). Multiple END blocks run in reverse order (LIFO) — the last-declared runs first, which is the right order for teardown of nested resources.
Deep Dive The $] variable holds the Perl version as a number (e.g. 5.038001). $^O is the operating system name. $0 is the script name. $$ is the current PID. These special variables, documented in perlvar, are your runtime introspection toolkit.