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#

  1. A package defines a namespace. Any package can be a class; the difference is purely how it is used.

  2. A reference holds instance data. The convention is a hash reference, but any reference type works.

  3. bless tags 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 new is the constructor. It is an ordinary method; the language has no constructor keyword. By convention it is named new and returns the blessed reference.

  • my ($class, %args) = @_ unpacks the invocant first, then the named arguments. A class-method call File->new(path => ...) passes the class name as the first argument.

  • bless $self, $class attaches the class tag. Using $class (the actual invocant) rather than a hard-coded 'File' is what makes the constructor inheritance-safe: a subclass inheriting new will bless into the subclass’s name.

  • return is usually omitted after bless because bless returns its first argument. bless $self, $class as 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:

  • DESTROY runs at unpredictable times. Do not rely on it for correctness-critical cleanup — explicit $obj->close beats implicit destruction.

  • DESTROY can see a partially constructed object if new croaked mid-way. Defensive defined checks are cheap insurance.

  • During global destruction (END time), dependencies may already be gone. A DESTROY that walks references can fail with “Attempt to free unreferenced scalar” or similar. Wrap risky cleanup in eval { ... }.

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 class form 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 class feature is unavailable.

  • You need array-backed or code-backed objects. class only creates hash-backed instances. If you are building a performance- critical structure with array-reference instances, classical bless is still the answer.

  • You are doing something the class feature deliberately forbids — reopening the package from outside, multiple inheritance, or deep metaprogramming via @ISA manipulation.

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 class feature. 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.