Classical OO#
Before the class feature landed in 5.38, Perl’s OO was built from three primitives: a package, a reference, and the bless function. Those three primitives are still present - the modern class keyword sits on top of the same machinery - and every pre-5.38 codebase, plus most of CPAN, still uses them directly.
This chapter is for reading and maintaining that code. If you are writing a new class, start with the modern class chapter instead.
The three primitives#
A package defines a namespace. Any package can be a class; the difference is purely how it is used.
A reference holds instance data. The convention is a hash reference, but any reference type works.
blesstags the referent with a class name, so that method dispatch through the reference looks up subs in that package.
That is the whole model. Everything else - constructors, accessors, inheritance, roles - is a convention built on top.
A minimal class#
package File;
sub new {
my ($class, %args) = @_;
my $self = {
path => $args{path},
content => $args{content},
};
return bless $self, $class;
}
sub path { $_[0]{path} }
sub content { $_[0]{content} }
sub print_info {
my $self = shift;
print "This file is at $self->{path}\n";
}
1;
Key conventions, each load-bearing:
sub newis the constructor. It is an ordinary method; the language has no constructor keyword. By convention it is namednewand returns the blessed reference.my ($class, %args) = @_unpacks the invocant first, then the named arguments. A class-method callFile->new(path => ...)passes the class name as the first argument.bless $self, $classattaches the class tag. Using$class(the actual invocant) rather than a hard-coded'File'is what makes the constructor inheritance-safe: a subclass inheritingnewwill bless into the subclass’s name.returnis usually omitted afterblessbecauseblessreturns its first argument.bless $self, $classas the last expression in the sub is idiomatic. We write it explicitly here for readability.
Accessor methods by hand#
The model gives you no accessors; you write them. The terse form exploits the fact that $_[0] is the invocant:
sub path { $_[0]{path} }
sub content { $_[0]{content} }
The named-parameter form is clearer and what you should write when the accessor does anything non-trivial:
sub path {
my $self = shift;
return $self->{path};
}
A read/write accessor:
sub name {
my $self = shift;
$self->{name} = shift if @_;
return $self->{name};
}
This is the boilerplate the modern class feature replaces with :reader and :writer.
Why hash references#
Hash references dominate because they let you add and rename fields without changing the layout, and because named keys survive a Data::Dumper or JSON::encode round-trip. Array references are faster and more compact but fragile: renumbering a slot breaks every access site.
Hash-backed instances have one serious drawback: no encapsulation. Any caller can reach into the object’s internals with $obj->{path}. The classical model has no tool to stop them - self-discipline and code review are the only defences. This is the single biggest reason to prefer the modern class feature for new code.
Inheritance with @ISA#
Inheritance is a package-level list named @ISA. Method dispatch walks it when the current package does not define the method.
package File::MP3;
our @ISA = ('File');
sub print_info {
my $self = shift;
$self->SUPER::print_info;
print "Title: $self->{title}\n";
}
1;
SUPER:: means “look for this method starting one step up the ISA chain from the package the current method was compiled in”
not from the object’s class. That subtlety matters in diamond inheritance; the inheritance chapter covers it.
Declaring the parent - four ways#
There are four spellings of the same idea. Prefer the third for new code you are still maintaining in the classical style.
# 1. Raw @ISA assignment.
package Child;
our @ISA = ('Parent');
# 2. use base.
package Child;
use base 'Parent'; # deprecated in modern Perl
# 3. use parent.
package Child;
use parent 'Parent'; # current recommendation for classical
# 4. @ISA with a require.
package Child;
require Parent;
our @ISA = ('Parent');
use parent handles the load of the parent module and the @ISA push in one line. use base is an older variant that also imports Exporter-style fields; avoid it.
The $self invocant#
Every method is a subroutine whose first argument is either the class name (for class methods like new) or the object (for instance methods). The convention is to shift it into a variable named $self:
sub method {
my $self = shift;
...
}
Or unpack it together with other arguments:
sub method {
my ($self, $arg1, $arg2) = @_;
...
}
There is no runtime enforcement. If you forget to shift $self, your first user-supplied argument silently becomes $self and chaos ensues. Static analysers catch this; the language does not.
Destructors#
A DESTROY method, if defined, runs when the object’s refcount falls to zero. Use it to release external resources:
sub DESTROY {
my $self = shift;
close $self->{fh} if defined $self->{fh};
}
Pitfalls that bite every project at least once:
DESTROYruns at unpredictable times. Do not rely on it for correctness-critical cleanup - explicit$obj->closebeats implicit destruction.DESTROYcan see a partially constructed object ifnewcroaked mid-way. Defensivedefinedchecks are cheap insurance.During global destruction (
ENDtime), dependencies may already be gone. ADESTROYthat walks references can fail with “Attempt to free unreferenced scalar” or similar. Wrap risky cleanup ineval { ... }.
AUTOLOAD#
If a method call finds no matching sub in the class or its ancestors, Perl looks for an AUTOLOAD sub. The called method’s name appears in our $AUTOLOAD:
our $AUTOLOAD;
sub AUTOLOAD {
my $self = shift;
my $name = $AUTOLOAD;
$name =~ s/.*:://;
return if $name eq 'DESTROY'; # critical
...
}
The DESTROY guard is mandatory: without it, object destruction triggers AUTOLOAD with $name = 'DESTROY' and you either misbehave or crash.
AUTOLOAD is powerful and easy to abuse. The cases it earns its keep:
Lazy accessor generation - synthesise an accessor on first call, then install it as a real sub via typeglob assignment so the next call is direct.
Forwarding - delegate unknown methods to a contained object (but see Roles and delegation for a cleaner approach).
Cases where it hurts more than it helps: pretending to be a full-blown MOP, catching typos as silent no-ops, implementing “classes” whose entire surface is AUTOLOAD.
A worked example#
A classical Counter class with read/write accessor and inheritance:
package Counter;
sub new {
my ($class, %args) = @_;
my $self = {
value => $args{value} // 0,
step => $args{step} // 1,
};
return bless $self, $class;
}
sub value {
my $self = shift;
$self->{value} = shift if @_;
return $self->{value};
}
sub step { $_[0]{step} }
sub tick {
my $self = shift;
$self->{value} += $self->{step};
}
1;
A subclass that doubles the step:
package Counter::Double;
use parent 'Counter';
sub new {
my ($class, %args) = @_;
$args{step} //= 2;
return $class->SUPER::new(%args);
}
1;
Compared with the modern class version, the classical form is about three times as many lines and every one of them is a place to get it wrong.
When classical OO is the right call#
Despite everything, there are legitimate reasons to stick with bless-based classes:
You are maintaining existing code. Rewriting a working classical class into
classform is a migration project, not a free lunch - see the migration chapter.You need to support older Perl. If your code must run on Perl before 5.38, the
classfeature is unavailable.You need array-backed or code-backed objects.
classonly creates hash-backed instances. If you are building a performance- critical structure with array-reference instances, classicalblessis still the answer.You are doing something the
classfeature deliberately forbids - reopening the package from outside, multiple inheritance, or deep metaprogramming via@ISAmanipulation.
A historical note on CPAN OO systems#
Because the classical model leaves so much to convention, CPAN grew an ecosystem of modules that layer declarative syntax on top:
Moose - feature-complete OO system with a type miniature, roles, method modifiers, a full introspection API, and a large extension ecosystem. Heavy at load time; the dominant choice for large applications built between 2007 and roughly 2020.
Moo - a subset of Moose’s API without the introspection layer. Lighter, pure-Perl, API-compatible enough to let a Moo class and a Moose class share an inheritance tree.
Class::Accessor - generates simple accessors and
new. Minimal, pure-Perl, no roles.Class::Tiny - the smallest of the accessor-generator modules. All accessors are read/write, no type checks.
Object::Pad - the experimental prototype that became the core
classfeature. Still installable on CPAN for code that needs the feature on older Perls.Role::Tiny - role composition for projects that do not use Moose but still want
does-style sharing.
These systems are historical context in pperl. New code in pperl should use the core class feature, which covers the ground that Moose/Moo pioneered without the load-time and dependency cost. When you inherit a codebase that uses one of them, learn just enough of its surface to stay productive and plan the migration with the migration chapter.