TITLE
Migrating from Classical Perl to Object::Pad
INTRODUCTION
Object::Pad provides a convenient and modern syntax for writing code in a class-based object oriented style. It is likely you already have much code already written using Perl's original style, of manual calls to bless
, storing instance data directly in hash keys, and so on. This guide aims to provide a sequence of steps to help rewrite this kind of code into using Object::Pad
instead.
As well as being useful on its own, this can often serve as the first step towards a further onwards migration to Feature::Compat::Class, and eventually the native class syntax provided by recent version of Perl itself as the 'class' feature. See also perlclass.
SCENARIO 1 - A SIMPLE CLASS
Lets suppose we have a simple module that provides some sort of object class based on a blessed hash reference. Before we start, the module file begins
use v5.36;
package My::Example::Class v1.23;
sub new
{
my $class = shift;
my %params = @_;
return bless {
x => $params{x} // 0,
y => $params{y} // 0,
}, $class;
}
...
Over the following steps we will look at a sequence of small changes that can be made to this file, to turn it into using Object::Pad
in a good style. These steps are self-contained, in that each can be made one at a time, while the code remains fully operational with its intended behaviour, in-between. Each of these changes is also entirely internal within the source file that implements this class. The externally-visible API to this class that other code will see remains entirely unmodified.
As a result of this, you do not have to migrate everything all at once. You can start by altering just a few files, or making just the first few changes to some files, and the system or application as a whole will remain running just as it did before. This allows you to gradually rewrite in stages, without needing to perform one big distruptive change.
Step 1 - Basics
The first thing to do is to add the use Object::Pad
line at the start. This gives us several new keywords - most notably the class
keyword which we'll use for declaring the class itself. Don't worry about imports polluting the main::
namespace - the Object::Pad
module only has lexical effects, so the scope of its additions is limited to this file alone.
We'll give a module version number to this use
statement. This is important, because we want to ensure we have a sufficiently recent version of the module for our current set of features.
Now our file can begin
use v5.36;
use Object::Pad v0.800;
package My::Example::Class v1.23;
sub new
{
...
}
Next up, we need to declare our class by using the class
keyword. This can be used in place of the package
keyword. As we are in the process of migrating some existing code, we need to temporarily tell Object::Pad
to do something odd with the object instances.
Normally for a newly-written class, it will pick its own internal representation type for instances, that is largely opaque to outside interaction. This internal type may be some kind of array reference on older Perl versions, but on versions of Perl new enough to support the class
feature it actually uses the same opaque representation type that core Perl classes will use.
Until we have finished our migration process for this class, we need to ensure it uses a blessed hash reference so that existing code we've yet to rewrite continues to work correctly. Much of this code will presume that object instances are hash references and will attempt to use individually named keys within these references to store their data. For this to keep working, we need to supply the :repr
attribute, and ask to specifically use blessed hash references.
use v5.36;
use Object::Pad v0.800;
class My::Example::Class v1.23 :repr(HASH);
sub new
{
...
}
Step 2 - Constructor
We could at this point try running the code already, although in practice we haven't really changed anything yet. If we did we'd get one warning already:
Subroutine new redefined at ...
This warning comes because Object::Pad
has already provided a constructor method, named new
, so our code doesn't have to. The next thing we need to do, then, is to get rid of our current new
method and move the code elsewhere.
Looking in more detail at our original code, we can take a look at the new
method. This takes a hash of incoming parameters, and extracts a couple of values to use as instance fields, along with defaulting values in case they are missing. This includes the bless
expression itself.
sub new
{
my $class = shift;
my %params = @_;
return bless {
x => $params{x} // 0,
y => $params{y} // 0,
}, $class;
}
We can replace this constructor with a BUILD
phaser. This is a named block, much like BEGIN
or similar in core Perl, which provides some code that runs at a particular time. The code in this block runs as part of the constructor that Object::Pad
provided for us.
Much like with the :repr
attribute we added earlier, this block is more of a temporary tool to aid the process of migrating existing code. It wouldn't be used in a newly-written native class - there are better things to use there. But for now, it is required because it gives us a way to handle the incoming arguments to the constructor and set up the initial values of those fields.
Within the scope of the BUILD
phaser, a lexical variable called $self
is implicitly visible; its value will be a reference to the object being constructed. This will be a common theme later on - you don't need to specifically handle this as an argument; it is done automatically. When the BUILD
phaser runs, it receives as any extra arguments into its @_
array all of the additional values that the caller passed to the constructor. So we can inspect them there in the same way.
We can now replace the entire sub new
with the following BUILD
phaser
BUILD
{
my %params = @_;
$self->{x} = $params{x} // 0;
$self->{y} = $params{y} // 0;
}
Since we're on a version of Perl that is newer than v5.26
, we can use subroutine signature syntax to further tidy this block up. Like subroutines (and as we'll see later on, methods), these BUILD
phasers can be annotated with a signature, to automatically unpack the arguments passed in. We can write this even shorter.
BUILD ( %params )
{
$self->{x} = $params{x} // 0;
$self->{y} = $params{y} // 0;
}
Step 3 - Method
Now lets turn our focus to the other methods in the file. Most likely there were some existing methods designed as field accessors, and perhaps other behaviour that actually performs some real work. For the sake of providing some interesting variety of styles to migrate from, lets look at a few different ways the existing code might have been written.
sub x { return shift->{x}; }
sub set_x { $_[0]->{x} = $_[1]; }
sub y ( $self )
{
return $self->{y};
}
sub set_y ( $self, $new_y )
{
$self->{y} = $new_y;
}
sub reset
{
my $self = shift;
$self->{x} = $self->{y} = 0;
}
In each of these cases, we can use another of Object::Pad
's new keywords, method
. A method
declaration is similar to a sub
, except that it automatically handles the implicit $self
argument at the beginning of the argument list. In each method body, we already have such a variable in scope without needing to have explicitly created it ourselves.
method x { return $self->{x}; }
method set_x { $self->{x} = $_[0]; }
method y
{
return $self->{y};
}
method set_y ( $new_y )
{
$self->{y} = $new_y;
}
method reset
{
$self->{x} = $self->{y} = 0;
}
In particular with the set_x
method, note that since the implicit $self
has been shifted out of the arguments array, the new value for the field now appears at $_[0]
, not $_[1]
as it had done prior.
Like we did earlier with the BUILD
phaser, we can additionally make consistent use of subroutine signatures while we're here, to have some neater handling of the other arguments passed in to these method. While we're at it it's always good practice to mark an empty signature ()
on any methods we're not expecting to pass additional arguments into, so that at runtime it will complain if someone accidentally does.
method x () { return $self->{x}; }
method set_x ( $new_x ) { $self->{x} = $new_x; }
method y () { return $self->{y}; }
method set_y ( $new_y ) { $self->{y} = $new_y; }
method reset ()
{
$self->{x} = $self->{y} = 0;
}
Step 4 - Fields
One of the key benefits of using Object::Pad
over plain classical Perl style is that all of the fields that store data within each instance are accessible using syntax that makes them look like lexical variables, rather than hash-key access via $self
. All of our changes so far have been leading up to the ability to do exactly this. So lets do that now.
So far in all of our code, we have only used fields $self->{x}
and $self->{y}
. We can now declare those two using the field
keyword, and replace all of the occurances in existing code with those names.
field $x;
field $y;
BUILD ( %params )
{
$x = $params{x} // 0;
$y = $params{y} // 0;
}
method x () { return $x; }
method set_x ( $new_x ) { $x = $new_x; }
method y () { return $y; }
method set_y ( $new_y ) { $y = $new_y; }
method reset ()
{
$x = $y = 0;
}
At this point it may be that there is now no longer any code left which tries to access $self
as if it was a hash reference. All of the basic field access in these methods has been updated to use the real field
variables. If we're sure we have no other code left in this file, and no other code elsewhere that, for example, tries to make any subclasses of this class, then it will be safe to remove the :repr(HASH)
attribute on the class
line.
use v5.36;
use Object::Pad v0.800;
class My::Example::Class v1.23;
If not, there is no great trouble in it remaining there for a while longer, until a wider and more complete migration of the entire codebase has been completed. They can be tidied up at the end.
Step 5 - Convenience Accessors
At this point, you may notice a common pattern with the accessor methods we defined. We have two reader methods that simply return the current value of the fields, and two writer methods that simply set a new value.
As this is such a common pattern, Object::Pad
provides some attributes you can annotate onto a field
declaration to have it build these methods for you. These attributes are called :reader
and :writer
.
field $x :reader :writer;
field $y :reader :writer;
The :reader
attribute will create a reader method identical to the ones we manually created in the previous example. By default each method will be named as per the field it is attached to.
Likewise, the :writer
attribute will create a writer method identical to the ones we created as well. Its naming convention is that it will prepend set_
to the name of the field, so once again its default behaviour already matches the names of the accessors we wish to be created.
With these in place we can entirely delete our manually-written x
, set_x
, y
and set_y
methods.
Step 6 - Parameters and Field Defaults
Recall earlier that we created the BUILD
block as a temporary migration tool to help handle constructor parameters. Now that we have real field
declarations it is time to fix that up into a more appropriate way of working.
As was the case with the :reader
and :writer
attributes on a field, there is another attribute called :param
that can be applied which requests some implicit behaviour by Object::Pad
itself, to assign the value of a field from a named parameter passed to the constructor.
field $x :reader :writer :param;
field $y :reader :writer :param;
As it stands, these create named parameters to the class constructor that act much like signature parameters without defaulting expressions - in that, they are mandatory. An error is raised at runtime if a caller does not provide a corresponding value for one.
My::Example::Class->new( x => 100 )
Required parameter 'y' is missing for My::Example::Class constructor at ...
In order to match the behaviour of the original sub new
and later the BUILD
phaser we temporarily added, we should provide a default value for these fields that will be applied if the caller did not pass in a more specific one. Since the original behaviour applied the value 0
using the //
operator, we can preserve this same behaviour by using the //=
assignment operator. This applies the default if the named parameter was absent, or given as the value undef
.
field $x :reader :writer :param //= 0;
field $y :reader :writer :param //= 0;
As these defaulting operators entirely provide the behaviour we wrote manually in the BUILD
phaser, we can delete that too.
The End Result
Now we have finished these steps, we have fully converted our original class that was written using classical Perl with manual bless
expressions into using all of the conveniences and features of the object system provided by Object::Pad
. Many of the behaviours that had been explicitly provided in manually-written code are now implied by standard features of the object system. This makes them easier to comprehend at a quick glance. There is no custom code to have to read and understand, instead the mere presence of the standard keywords and attributes.
Having started with the customly-written code at the beginning, we are now left with far fewer lines. The entire behaviour is now captured by this:
use v5.36;
use Object::Pad v0.800;
class My::Example::Class v1.23;
field $x :reader :writer :param //= 0;
field $y :reader :writer :param //= 0;
method reset ()
{
$x = $y = 0;
}
Addendum - Conventions on Field Names
Since these field variables are lexically scoped at the entire file level, in larger code examples it can sometimes be hard to remember that they even are field variables, confusing them with signature parameters or lexicals within individual methods and smaller bodies of code. With the previous style of using blessed hash references, the $self->{...}
pattern gives an obvious visual clue, but that kind of clue is lacking here.
It is a common style choice in larger classes to give field variables names all beginning with a single underscore character, to distinguish them. This naming style makes it a little easier to see at a glance when looking at code that may be far away from the field declarations, that these variables even are fields.
field $_x;
field $_y;
BUILD ( %params )
{
$_x = $params{x} // 0;
$_y = $params{y} // 0;
}
method reset ()
{
$_x = $_y = 0;
}
...
Since Object::Pad
knows about the naming convention of beginning each field variable with a leading underscore it will ignore that for the purposes of things like handling constructor arguments for :param
, or generating accessor methods for :reader
and :writer
. Thus the methods are still named x
and y
as we would like.
AUTHOR
Paul Evans <leonerd@leonerd.org.uk>