Professional Perl | 16
[previous] [next] |
Professional Perl
Prototyping Code References
Other than $, @ (and the synonymous %), we can supply one other basic prototype character: &. This tells Perl that the parameter to be supplied is a code reference to an anonymous subroutine. This is not as far-fetched as it might seem; the sort function accepts such an argument, for example.
Here is how we could prototype the do_list subroutine we introduced when we covered anonymous subroutines earlier:
sub do_list (&@) {
my ($subref, @in) = @_;
my @out;
foreach (@in) {
push @out, &$subref ($_);
}
return @out;
The prototype requires that the first argument be a code reference, since the subroutine cannot perform any useful function on its own. Either a subroutine reference or an explicit block will satisfy the prototype; for example:
@words = ("ehT", "terceS", "egasseM");
do_list {print reverse($_[0] =~/./g), "\n"} @words
Note how this syntax is similar to the syntax of Perl's built-in sort, map and grep functions.
Subroutines as Scalar Operators
We mentioned previously that subroutines can be thought of as user-defined list operators, and used much in the same way as built-in functions (that also work as list operators) like print, chomp, and so on. However, not all of Perl's functions are list operators. Some, such as abs, only work on scalars, and interpret their argument in a scalar context (or simply refuse to execute) if we try to supply a list.
Defining subroutines with a prototype of ($) effectively converts them from being list operators to scalar operators. Returning to our capitalize example, if we decided that, instead of allowing it to work on lists, we want to force it to only work on scalars, we would write it like this:
sub capitalize ($) {
$_[0] = ucfirst (lc $_[0]);
However, there is a sting in the tail. Before the prototype was added this subroutine would accept a list and capitalize the string in the first element, coincidentally returning it at the same time. Another programmer might be using it in the following way, without our knowledge:
capitalize (@list)
While adding the prototype prevents multiple strings being passed in a list, an array variable still fits the prototype, as we saw earlier. Suddenly, the previously functional capitalize turns the passed array into a scalar number:
@countries = ("england", "scotland", "wales");
capitalize (@countries)
The result of this is that the number '3' is passed into capitalize. Since this is not a variable, it causes a syntax error when we try to assign to $_[0]. If we chose to return a result rather than modifying the passed argument, then the code would all be perfectly valid, but badly bugged. However, a program that is used to print 'England' might start printing '3' instead. This is more than a little confusing, and not intuitively easy to track down.
The key problem here is not that we are passing an array instead of a scalar, but that we are checking for a scalar value rather than a scalar variable, which is what we actually require. In the next section we will see how to do that.
Requiring Variables rather than Values
So far we have seen how to enforce a specific number of arguments and their scope, if not their data type. We can also use prototypes to require that an actual variable be passed. This is invaluable when we want to implement a subroutine that modifies its passed parameters, such as the capitalize example just above.
To require a variable, we again use a $, @ and % character to specify the type, but now we prefix it with a backslash. This does not, as it might suggest, mean that the subroutine requires a reference to a scalar, array, or hash variable. Instead, it causes Perl to require a variable instead of merely a value. It also causes Perl to automatically pass the variable as a reference:
#!/usr/bin/perl
# varproto.pl
use warnings;
use strict;
sub capitalize (\$) {
${$_[0]} = ucfirst (lc ${$_[0]});
my $country = "england";
capitalize $country;
print $country, "\n";
# capitalize "scotland"; # ERROR: compile-time syntax error!
If we tried to call capitalize with a literal string value, we would get the error:
Type of arg 1 to main::capitalize must be scalar (not constant item) at ..., near ""england";"
The fact that Perl automatically passes variables as references is very important, because it provides a new way to avoid the problem of list flattening. In other words, prototypes allow us to pass arrays and hashes to a subroutine as-is, without resorting to references in the subroutine call.
A push is an example of a built-in function that works by taking an array as its first argument. We do not need to treat that variable specially to avoid flattening, and we can replicate that syntax in our own code by defining a prototype of (\@@). The following subroutine uses the list-processing version of capitalize to produce a capitalizing push subroutine. First it removes the array variable using shift, then capitalizes the rest of the arguments and adds them to the variable with push. Perl, being versatile, lets us do the whole thing in one line:
sub pushcapitalize (\@@) {
push @{shift}, capitalize(@_);
We can use this subroutine just like we use the push function.
pushcapitalize @countries, "england"; pushcapitalize @countries, "scotland", "wales"; pushcapitalize @countries, @places; # no flattening here!
Note that we omitted the parentheses, which requires that the subroutine be either already defined or predeclared.
Hash variables are requested using \%, which unlike % does have a different meaning to its array counterpart \@. Here is an example that flips a hash variable around so that the keys become values and the values become keys. If two keys have the same value one of them will be lost in the transition, but for the sake of simplicity we'll ignore that here:
sub flip (\%) {
@hash = %{$_[0]};
%{$_[0]} = reverse @hash;
This subroutine makes use of the fact that a hash is essentially just a list with an even number of values, and a little extra cleverness allows quick key access. So, to flip the hash we turn it into a list and reverse it. This also reverses each key-value pair with respect to each other; we then turn it back into a hash again.
Although Perl will automatically pass variables as references when a variable prototype is in effect, it will allow an explicit reference if we dereference it first. The two following calls are both valid uses of the above subroutines:
For the pushcapitalize subroutine:
pushcapitalize @{$countries_ref}, "england"
And for the flip subroutine:
flip %{$hash_ref}
Before we finish with variable prototypes it is worth mentioning, just for completeness, that \& also has a meaning subtly different from &. It requires that the passed code reference be a reference to an actual subroutine, that is, a code reference defined using $coderef = sub {...} or $coderef = \&mysubroutine. A reference to an in-line bare block (such as in mysub {...} @list) will not be accepted. Another way to look at \ is that it requires that the argument actually starts with the character it precedes: \& therefore means that the argument must start &, not {.
[previous] [next] |
Created: April 3, 2001
Revised: April 3, 2001