Perl is what's known as a weak typed language. Rather than having separate data types for numbers, strings, and each types of object, there's just one type - a scalar - that can be used to hold a number, a string or a reference to an object. This has great advantages as you can code your code to be a lot more flexible. For example in Java you'd have to write a method for each of the possible arguments your method can be passed. In Perl you could write one method that automatically fills in default values for the majority of the arguments and otherwise is complicit with the arguments - automatically converting one type of object into another as needed.
It has a lot of disadvantages too. If you can't trust code to pass your methods the right arguments (and if it's a public method then you really shouldn't) then you have to check the data that was passed though your parameters is valid - that you haven't been passed a string where you expected a number, or an object of completely the wrong type. Strongly typed languages of course will do all of this for you.
This kind of code is tedious for you to write, and it's the kind of thing where you can make simple errors that introduce subtle bugs into your code. This is where Params::Validate can be of use. It's an automated tool that can be used to do a lot of checking for you - in most cases enough.
For this example we're going to look at a hypothetical piece of code. As is a London.pm tradition, I'll use a Buffy The Vampire Slayer related example...
use Slayer; my $slayer = Slayer->new(name => "Buffy Summers", skills => ["Cheerleading", "Shopping", "Banter"], age => 15, hair => "blonde");
$slayer->stake( monster => $monster, with => $broom);
One of the most common idioms for passing arguments to Perl methods is the named parameter passing trick. This is where you shift off the class or object, and then assign the rest of the arguments to a hash.
sub new { my $self = bless {}, shift; my %args = @_
print STDERR "Creating '$args{age}' year old '$args{name}'\n" if DEBUG; ...
return this; }
sub stake { my $self = shift; my %args = @_;
... }
Okay, let's see what we can do with Params::Validate. For a start, we want to ensure that our constructor always has a name, some skills, and an age, and optionally they can pass in the hair colour they want.
use Params::Validate qw(:all);
sub new { my $self = bless {}, shift; my %args = validate(@_, { name => 1, skills => 1, age => 1, hair => 0, });
...
return $self; }
We replace the @_
with a call to the subroutine validate
that we
pass @_
and a hashref that contains the specification that we want
to validate @_
against. If validation fails, an exception will be
thrown. The above shows the most simple specification, where we
specify that our new slayer must have all three attributes. We can do
a little better than this by specifying a list of types that the data
must have by replacing the 1
with a specification hash for that
parameter. Pay attention to the fact that SCALAR
and ARRAYREF
in the following example aren't strings - they're constants exported by
Params::Validate
; Putting them in ""
won't work.
sub new { my $self = bless {}, shift; my %args = validate(@_, { name => { type => SCALAR }, skills => { type => ARRAYREF }, age => { type => SCALAR }, hair => { type => SCALAR, optional => 1 },});
...
return $self; }
But wait, what if we don't want to have to specify skills for every new slayer? It's pretty much a given that each new slayer will have the amazing ability to come up with crazy backchat during fights, so we want to assign the skill of Bantering as standard if no skills are passed. We can do that with the default option:
sub new { my $self = bless {}, shift; my %args = validate(@_, { name => { type => SCALAR }, skills => { type => ARRAYREF, default => ["Banter"] }, age => { type => SCALAR }, hair => { type => SCALAR, optional => 1 },});
...
return $self; }
Now the skills are optional and the default will be used if no argument is supplied. So now we can create a new slayer like so, without having to specify any skills:
my $faith = Slayer->new(name => "Faith", age => "17",);
Note that the default is only used when the parameters are omitted - if you supply something that isn't up to spec the default won't be used - an error will be thrown as usual. Now let's turn our attention to the stake method. It takes two parameters, who our slayer is staking, and what with.
sub stake { my $this = shift; my %args = validate(@_, { monster => 1, with => 1,}
# call the stake's 'dust' method and up our kill counter $args{with}->dust($args{monster}); $this->{killed}++ }
But wait! We know Buffy can only stake Vampires. We'd better check
that $vampire
was actually a Vampire
object. And we'd better
check we're using a subclass of Stake
(a Stake
object, a
Stake::Broom
, a Stake::Rake
, etc) to do it with.
sub stake { my $this = shift; my %args = validate(@_, { monster => { isa => "Vampire" }, with => { isa => "Stake", }}); # call the stake's 'dust' method and up our kill counter $args{with}->dust($args{monster}); $this->{killed}++ }
Hang on a moment though, Buffy can 'stake' Vampires with any old sharp
bit of wood. She can smash up chairs. So they don't really have to
be a subclass of Stake after all. Essentially all we want is an
object that supports the dust
interface (i.e. any object that has a
dust
method.) We can do that with the can test:
sub stake { my $this = shift; my %args = validate(@_, { monster => { isa => "Vampire" }, with => { can => [qw(dust)], }}); # call the stake's 'dust' method and up our kill counter $args{with}->dust($args{monster}); $this->{killed}++ }
So you can see all these validations offer us the chance to check our inputs with some inbuilt tests. But what happens if there's not a test for our parameter? What happens if we want to make sure that some fool hasn't written
my $slayer->new(name => "Kendra", age => "seventeen");
Well, Param::Validate allows us to write extension tests with callback facilities. This allows you to define subroutines that will be called with the parameter that should return true if the parameter passes.
sub new { my $self = bless {}, shift; my %args = validate(@_, { name => { type => SCALAR }, skills => { type => ARRAYREF , default => ["Banter"] }, age => { type => SCALAR, callbacks => { digits => sub { $_[0] =~ /^\d+$/; }, teens => sub { $_[0]>12 && $_[0]<20 }, } }, hair => { type => SCALAR, optional => 1 }, });
...
return $self; }
And that's pretty much it. Of course, Params::Validate has a whole section of code dedicated to dealing with positional rather than named parameters that I haven't covered in this mini-tutorial. I'll leave it as an exercise for the reader.