Professional Perl | 15
[next] |
Professional Perl
Prototypes
The subroutines we have considered so far exert no control over what arguments are passed to them; they simply try to make sense of what is passed inside the subroutine. For many subroutines this is fine, and in some cases allows us to create subroutines that can be called in a variety of different ways. For example, we can test the first argument to see if it is a reference or not, and alter our behavior accordingly. However, we are not enforcing a calling convention, so we will only discover our subroutines are being called incorrectly when we actually execute the call, and then only if we have written the subroutine to check its arguments thoroughly. Since some subroutine calls may not occur except under very specific circumstances, this makes testing and eliminating bugs very difficult.
Fortunately there is a way to define compile-time restrictions on the use of subroutines through the use of prototype definitions. Although entirely optional, by specifying the types of the expected parameters, prototypes can eliminate a lot of the problems involved in ensuring that subroutines are called correctly. This allows us to specify what parameters a subroutine takes (scalars, lists/hashes or code references), and whether a parameter can be either a simple literal value, or whether it must be an actual variable. Good use of prototypes early in the development process can be invaluable.
A prototype definition is a parenthesized list of characters mirroring the Perl variable type syntax (that is, $, @, %, and so on). It is placed after the sub keyword and subroutine name but before anything else, be it a subroutine definition, declaration, or anonymous subroutine:
sub mysub (PROTOTYPE); # subroutine declaration
sub mysub (PROTOTYPE) {...} # subroutine definition
$subref = sub (PROTOTYPE) {...} # anonymous subroutine
Defining the Number of Parameters and their Scope
Prototypes allow us to explicitly define how many arguments a subroutine expects to receive. This is something that for efficiency reasons we would clearly prefer to check at compile-time. We do not have to wait until the subroutine call is used to find out that it is faulty, and passing the wrong number of parameters is an obvious candidate for a bug.
To illustrate, consider the volume subroutine that we defined in various different forms earlier. With the exception of the named argument example, the subroutine expects three scalar parameters. Using prototypes we can enforce this by adding ($$$), meaning three mandatory scalar arguments, to the subroutine definition:
sub volume ($$$) {
# ... as before ...
With this prototype in place, volume can only be called with three scalar arguments. They can be literals or variables, but there must be three of them, and they must be scalar. Hence, this is legal:
print volume(1, 4, 9), "\n"; # displays 1 * 4 * 9 == 36
This, however, is not. Even though it provides the right number of values, it doesn't supply them in a way that fits the prototype:
@size = (1, 4, 9);
print volume(@size), "\n"
Instead, we get the error:
Not enough arguments for main::volume at ... near @size
So far, so good. However, due to Perl's concept of context, prototypes do not enforce things quite as strictly as this might imply. The prototype does not actually enforce a data type - it attempts to force it. What the first $ in the prototype actually does is force @size to be interpreted in scalar context and not as a list, in other words, it is exactly as if we had written:
print volume(scalar @size), "\n"
Having turned the three element array into a scalar '3', the prototype goes on to interpret the second argument as a scalar also. It then finds there isn't one, and produces an error. The fact that we passed an array is not relevant, since an array can be converted to a scalar. However, by passing just one array, we omitted two mandatory arguments, which is important. To illustrate this, the following actually works just fine, the array not withstanding:
print volume(@size, 4, 9); # displays 3 * 4 * 9 == 108
We have not supplied three scalars, but we have supplied three values which can be interpreted as scalars, and that's what counts to Perl.
We can also use @ and % in prototype definitions, and it is sometimes helpful to consider subroutines without prototypes as having a default prototype of (@); that is:
sub mysubroutine (@) {...
Just like unprototyped subroutines, the single @ prototype will absorb all values, flattening any lists or hashes it finds. It follows from this that a prototype of (@,@) is just as invalid as it was before. However, if we want to enforce an array variable, as opposed to a mere list, that's a different story, as we will see shortly.
A @ or % prototype matches all parameters in the argument list from the point it is defined to the end of the list. Indeed, % and @ are actually identical in meaning to Perl, since passing a hash turns it into a list. Recall that there is no such thing as 'hash context'. It cannot check that passed parameters came from a hash due to flattening, nor that the remaining parameters divide evenly into pairs because that is a run-time issue. However, this does not mean they are of no use. It means that the only useful place for either prototype character is at the end of the prototype. As an example, here is a subroutine, which joins array elements incorporating a prefix and suffix. It takes a minimum of three parameters, but has no maximum because of the @ prototype:
#!/usr/bin/perl
# join.pl
use warnings;
sub wrapjoin ($$$@) {
my ($join, $left, $right, @strings) = @_;
foreach (@strings) {
$_ = $left. $_. $right;
}
return join $join, @strings;
}
print wrapjoin("\n", "[","]", "One", "Two", "Three")
Without the @ we could only pass three arguments. If we added more $ characters we could allow more, but then we would be forced to supply that many arguments. The @ allows an arbitrary number, so long as we also supply three scalars to satisfy the initial $$$.
Lists can validly be empty, so the prototype does not ensure that we actually get passed something to join. We could attempt to fix that by requiring a fourth scalar, like this:
sub wrapjoin ($$$$@) {
($join, $left, $right, @strings) = @_;
However, a little thought reveals a flaw in this design. A literal list of strings works fine, but if the caller supplies an actual array variable for the fourth argument it gets converted to a scalar. In effect, we have introduced a new bug by adding the prototype.
The moral here is that prototypes can be tricky and can even introduce bugs. They are not a universal band-aid for fixing subroutine calling problems. If we want to detect and flag an error for an empty list, prototypes cannot help us - we will have to write the subroutine to handle it explicitly at run-time.
Contents |
[next] |
Created: April 3, 2001
Revised: April 3, 2001