[Wellington-pm] Gratuitous use of Perl Prototypes
Grant McLean
grant at mclean.net.nz
Mon Nov 8 16:06:27 CST 2004
Following on from my comments last night, I present the case against
inappropriate use of Perl prototypes.
But first, let's start with the case *for* appropriate use of Perl
prototypes. Imagine that you wanted to write a function that wrapped
Perl's builtin function 'push' but forced everything being pushed onto
the array to upper case. Your starting position might be this:
sub PUSH {
my(@array, @new_items) = @_;
push(@array, map { uc } @new_items);
}
my @target = ('RED', 'GREEN');
PUSH(@target, 'blue');
This obviously won't work since all arguments passed to PUSH() will end
up in @array and @new_items will end up empty. Another reason it won't
work is that the push function is operating on @array which only exists
within the PUSH function and doesn't affect the original array
(@target).
One way to make it work is to pass in an array reference instead of an
array:
sub PUSH {
my($array, @new_items) = @_;
push(@$array, map { uc } @new_items);
}
my @target = ('RED', 'GREEN');
PUSH(\@target, 'blue');
This is fine, but it does require a little more work from the caller
(actually one backslash) and it's not quite as tidy as Perl's builtin
'push'.
Prototypes allow your functions to behave like Perl's builtins. In
our example, we can use a prototype to declare that our PUSH function
needs one arrayref, followed by an arbitrarily long list of arguments:
sub PUSH (\@@) {
my($array, @new_items) = @_;
push(@$array, map { uc } @new_items);
}
my @target = ('RED', 'GREEN');
PUSH @target, 'blue';
Now when we call PUSH, we specify an array as the first argument and
Perl coerces that into the arrayref that PUSH requires.
Even though this is what Perl prototypes are intended to be used for,
it could be argued that even this use is a bad thing, since it might
lead to unexpected consequences. Usually in Perl if you pass an array
to a subroutine, you don't expect the array to be changed. If a
subroutine expects an arrayref, then that might be taken as a signal
that the routine possibly intends to modify the referenced array.
Prototypes allow a programmer to hide the fact that the routine will
get an arrayref.
Now let's open the case for the prosecution. Let's assume that we have
a persistent whiney developer who is determined to use prototypes on
every Perl subroutine and is attempting to defend his choice:
1. Prototypes allow Perl to pick up errors in subroutine calls.
This is a common mistake made by developers who have a background
with 'C', since this is exactly what 'C' prototypes are for.
It is unfortunate that Perl uses the name 'prototype', since in
Perl, the feature exists for an entirely different reason - to
allow Perl to silently coerce the arguments into a form that
matches the prototype. In fact Perl's compiler will only throw
errors if it can't manage to do that.
2. Prototypes are a useful way of documenting a subroutine's
expectations.
Perhaps, but a subroutine that starts like this:
sub border_style ($$$) {
only tells us that it expects three arguments, whereas a subroutine
that starts like this:
sub border_style {
my($width, $style, $colour) = @_;
tells us not only that the subroutine expects three arguments, but
also how it is planning to interpret each argument.
Since you're probably going to assign your arguments to variables
with meaningful names anyway, the prototype is essentially redundant.
If you need more documentation, then POD is the right tool for the
job.
3. Well at least I know my methods are being called with the right
number of arguments.
Whoa there! Did you say 'methods'? Perl does not attempt to
check prototypes at all for subroutines called as methods.
Remember, if your classes use inheritance, a method could be
defined in more than one place in the inheritance tree and it's
entirely possible that the calling signature for each might be
different. Since Perl doesn't know until runtime which subroutine
implements a method for a specific object, it can't check the
prototypes at compile time.
4. OK, so prototypes are not perfect, but at least they pick up some
classes of errors at compile time.
Perhaps, but used gratuitously, they can actually introduce some
errors at runtime. Consider this subroutine:
sub border ($;$$) {
my($width, $style, $colour) = @_;
$style = 'solid' unless defined $style;
$colour = 'black' unless defined $colour;
return "border: $width $style $colour;";
}
In theory, the prototype declares that the subroutine expects
one mandatory argument and two optional arguments (for which
the routine defines default values). So if we call it like
this:
print border('1px'), "\n";
it will return "border: 1px solid black;".
But if we call it like this:
my @args;
push @args, $selected ? '5px' : '1px';
push @args, 'dashed';
push @args, 'red' if $selected;
print border(@args), "\n";
Then if $selected is true, we might expect this return value:
"border: 5px dashed red;"
but we'd actually get:
"border: 3 solid black;"
The reason for this is that the prototype tells Perl we want the
first argument as a scalar, so it evaluates @args in a scalar
context which gives 3 (the number of elements in @args) and calls
the subroutine with the lone argument (3).
5. Well the coding standards for my project mandate the use of
prototypes
That's a bug in the coding standards. It might be correct for
other languages, but it's not correct for Perl.
6. My use of prototypes sends a clear message to people reading my code
that I am a careful coder.
You're right, it does send a clear message, just not the one you
think. People reading your code are pointing and sniggering as we
speak.
7. Alright you win.
Actually, if you stop the gratuitous use of prototypes, we all win.
In truth, the last time I went through this sermon we never got to
step 7 :-)
Cheers
Grant
More information about the Wellington-pm
mailing list