Declassifying Perl Method Arguments

I have plans to do an entire series of articles on the sheer awesomeness that is my native tongue (Perl).  I envision whole sections dedicated to setting up your environment, tips for beginners and intermediates alike, and maybe an advanced topic or two.  At that point, this blog post will firmly fall under the category of “Appendix“.

Nevertheless, this information was hard earned and will save me lots of time, so I wanted to share in the blessed bliss.

Module Rollin’ in Style

If you’ve ever created your own Perl module (or taken a peek at the source code to my sweet, sweet Metaphor project), then you have probably at least heard of Exporter.

The Exporter module implements an import method which allows a module to export functions and variables to its users’ namespaces. Many modules use Exporter rather than implementing their own import method because Exporter provides a highly flexible interface, with an implementation optimised for the common case.

Handy, right?  But then there’s this little tidbit – filed under What Not to Export -that tells me not to export anything by default.  Apparently, that is bad form.  Now read that quote again.  Wait, so you wrote this module to do exactly what you are suggesting we not use the module to do? Hmmmmm.

Robotic Escher Hands

Well, the whole point of Metaphor is automatic augmentation of my execution environment to provide my special style of syntactic sugar (aka a DSL ). So, your argument is invalid.

But It Got Me Thinking

And that’s how the hedgehog ended up winning the Ms. Cornbread Pageant. No, wait, wrong blog.  Sorry.

So, I can export methods using Exporter (explicitly or by default doesn’t matter at this point), but that doesn’t prohibit anyone from calling the method directly from the package or an object instance.  This presents a problem: since Perl doesn’t have a this keyword like many other languages, when a method is called in that fashion, it sends the package (class) or object which the method is called on as the first parameter to the method, and my exportable methods are not expecting that to ever be the first parameter!

[code language=”perl”]
use base Exporter;
our @EXPORT = qw(StaticMethod);

# If you are expecting the method to always be called in a
# non-exported fashion, you use the $self convention:
sub ClassMethod
{
my ($self, $host, $port) = @_;

}

# If you are anticipating the opposite, you do something like this:
sub StaticMethod
{
my ($host, $port) = @_;

}
[/code]

So, whether I export the method by default or not, I need a handy way to determine if my first parameter is a class or object so that I can discard it if I’m not expecting it.

But wait!  What if my method is expecting the first parameter to be a specific package or an object of a specific package?  Then I need a way to say, “Hey, if it’s one of these packages, then it’s all good if this is the first parameter”.

So I figured I’d write a method that did that detection for me, and make it an exportable method from my Metaphor::Util class.  Which means it needs to be able to be called both ways, too. I know. I know!

Introducing Declassify!

I started with a test module which – for convenience in my environment – I called Metaphor::Test.

[code language=”perl”]
package Metaphor::Test;

use strict;
use warnings;
use base ‘Exporter’;

our @EXPORT = qw(MethodOne);

sub new
{
return bless {}, shift;
}

sub MethodOne
{
my ($val) = @_;
print "$val\n";
}
1;
[/code]

Then, I wrote a few simple tests to ensure the output was as I expected.

[code language=”perl”]
use Metaphor::Test;

# 1 : use exported method
# output:
# desired : "1 : Hello"
# actual : "1 : Hello"
MethodOne("1 : Hello");

# 2 : use static method
# output:
# desired : "2 : Hello"
# actual : "Metaphor::Test"
Metaphor::Test->MethodOne("2 : Hello");

# 3 : use object method
# output:
# desired : "3 : Hello"
# actual : "Metaphor::Test=HASH(0x7b82e8)"
my $test = new Metaphor::Test();
$test->MethodOne("3 : Hello");
[/code]

We can see from this that while no runtime error is generated when accessing the methods each of the three different ways, only one of them actually does what I wanted.

What is needed is a way to guarantee that no matter how the method is called, it consistently works as expected.  For this, I wrote my now famous Declassify method, and exported it into my modules’ namespace.  Then I updated MethodOne to look like this:

[code language=”perl”]
sub MethodOne
{
my ($val) = Declassify(\@_, __PACKAGE__);
print "$val\n";
}
[/code]

And now all my method calls work as expected! I can even call Declassify directly – either off of a package or as an exported method – and get the same results.  Balance restored!

Hows That Go?

Pass Declassify a reference to the default array (like so: \@_) and the __PACKAGE__ variable, and it removes the reference (package or object reference) if it finds it at the front of the array, and returns the array.

Simple and straightforward, amiright? So grab the code here and don’t ever worry again about your exported methods being called in the wrong way.

And if the Perl::Critic::Policy::Subroutines::RequireArgUnpacking policy complains when you use Declassify – and it will! – you can ignore it.

Leave a Reply

Your email address will not be published. Required fields are marked *