DEV Community

Cover image for Learning Perl - Prototypes
LNATION for LNATION

Posted on

Learning Perl - Prototypes

Today we are going to discuss Perl subroutine prototypes, which are a way to enforce a certain structure on the arguments passed to subroutines. Prototypes can help catch errors early and make your code more readable.

In Perl, subroutine prototypes are used to define the expected number and types of arguments that a subroutine should receive. They are specified in the subroutine declaration and can include various modifiers. The prototype is placed immediately after the subroutine name. Here’s a simple example:

sub my_subroutine ($$) {
    my ($arg1, $arg2) = @_;
    print "Argument 1: $arg1\n";
    print "Argument 2: $arg2\n";
}
Enter fullscreen mode Exit fullscreen mode

In this example, the prototype ($$) indicates that my_subroutine expects exactly two scalar arguments. If you call it with the wrong number of arguments, Perl will throw an error.

You can also use prototypes to enforce that a subroutine receives a key-value list or hash. For example:

sub my_hash_subroutine (%) {
    my %args = @_;
    print "Arguments as hash:\n";
    foreach my $key (keys %args) {
        print "$key => $args{$key}\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, the prototype (%) indicates that my_hash_subroutine expects a hash as its argument. When you call it, you should pass a hash. If you do not for example you pass a odd number of arguments, Perl will again throw an error.

The following table summarises common Perl subroutine prototypes:

Prototype Meaning
'$' Scalar value
'@' Array value (flattens list)
'%' Hash value (flattens list)
'&' Code block (subroutine reference)
'*' Typeglob
';' Separates mandatory and optional arguments
'[$@%&*]' Reference to scalar, array, hash, code, or typeglob
'[]' Optional argument (used in documentation, not in actual prototypes)

Today we will apply this concept in a practical example, we will create a new functional Perl module that exports functions with prototypes to demonstrate there usage. Our module will be a basic statistics module that calculates the min, max, mean and median of a list of numbers. We will also allow them to specify a code reference as the first argument to be used to coerce the subsequent list. Think of it like custom grep or map keywords for basic statistics. Let's start by creating a new distribution using Module::Starter we will call it Stats::Basic.

module-starter --module="Stats::Basic" --author="Your Name" --email="your email"
Enter fullscreen mode Exit fullscreen mode

First lets add a new test file t/01-basic.t to test our module. We will start by accessing functions by namespace and add the exporting at the end. The first function we will implement is min, which will return the minimum value from a list of numbers. Add the following to the test file:

use Test::More;
use Stats::Basic;
my $min = Stats::Basic::min { $_ } 5, 1, 3, 2, 4;
is($min, 1, 'Minimum value is correct');
my $min = Stats::Basic::min { $_->{num} } (
    { num => 5 },
    { num => 1 },
    { num => 3 },
    { num => 2 },
    { num => 4 }
);
is($min, 1, 'Minimum value from hash is correct');

done_testing();
Enter fullscreen mode Exit fullscreen mode

Even some experienced Perl developers may not realise this, but when you define a subroutine prototype with a code reference as an argument, you don't explicitly use the sub keyword before the code reference. Perl automatically recognises that the argument is expected to be a code reference and will automagically figure it out, I find this to be a particularly elegant feature and will make our final implementation cleaner. Now let's implement the min function in our module. Open lib/Stats/Basic.pm and add the following code replacing the function1 placeholder:

=head2 min

Returns the minimum value from a list of numbers or a list of numbers.

    my $min = min { $_ } 5, 1, 3, 2, 4;

=cut

sub min (&@) {
    my ($code, @numbers) = @_;
    @numbers = sort {
        $a <=> $b
    } map { $code->($_) } @numbers;
    return $numbers[0];
}
Enter fullscreen mode Exit fullscreen mode

As you can see we have declared our prototype &@ this signifies to Perl that our function accepts a code reference as the first argument and a list as the second. We use map to iterate over the numbers calling the code reference on each item, then the result of that is passed to sort where we sort in ascending numerical order using $a <=> $b. Finally, we return the first element of the sorted array, which will be the minimum value.

If you now run your tests using prove you will see that our basic tests pass:

prove -lv t/
Enter fullscreen mode Exit fullscreen mode

Next lets extend our test file with tests for the max function.

my $max = Stats::Basic::max { $_ } 5, 1, 3, 2, 4;
is($max, 5, 'Maximum value is correct');
$max = Stats::Basic::max { $_->{num} } (
    { num => 5 },
    { num => 1 },
    { num => 3 },
    { num => 2 },
    { num => 4 }
);
is($max, 5, 'Maximum valus is correct');
Enter fullscreen mode Exit fullscreen mode

Now to implement we take a very similar approach to min but switch the sorting.

=head2 max

Returns the maximum value from a list of numbers or a list of numbers.

    my $max = max { $_ } 5, 1, 3, 2, 4;

=cut

sub max (&@) {
    my ($code, @numbers) = @_;
    @numbers = sort {
        $b <=> $a
    } map { $code->($_) } @numbers;
    return $numbers[0];
}
Enter fullscreen mode Exit fullscreen mode

With that in place run your tests again and all should pass. Next we will add the sum function. Lets first add the tests.

my $sum = Stats::Basic::sum { $_ } 5, 1, 3, 2, 4;
is($sum, 15, 'Sum value is correct');
$sum = Stats::Basic::sum { $_->{num} } (
    { num => 5 },
    { num => 1 },
    { num => 3 },
    { num => 2 },
    { num => 4 }
);
is($sum, 15, 'Sum value is correct');
Enter fullscreen mode Exit fullscreen mode

Now to implement this in lib/Stats/Basic.pm:

=head2 sum

Returns the sum value from a list of numbers.

  my $sum = sum { $_ } 5, 1, 3, 2, 4;

=cut

sub sum (&@) {
  my ($code, @numbers) = @_;
  my $sum = 0;
  map { $sum += $code->($_) } @numbers;
  return $sum;
}
Enter fullscreen mode Exit fullscreen mode

The logic is simple we have again defined the &@ prototype, this time we define a variable $sum initialised to 0, then use map to iterate over the numbers, applying our code reference to each item and adding the result to our running total. Finally, we return the accumulated sum. If you run your tests they will pass once again. The final function we are going to implement is mean. The mean is just the sum divided by the number of items so we should be able to reuse the function we just wrote. Lets first write the tests.

my $mean = Stats::Basic::mean { $_ } 5, 1, 3, 2, 4;
is($mean, 3, 'Mean value is correct');
$mean = Stats::Basic::mean { $_->{num} } (
    { num => 5 },
    { num => 1 },
    { num => 3 },
    { num => 2 },
    { num => 4 }
);
is($mean, 3, 'Mean value is correct');
Enter fullscreen mode Exit fullscreen mode

To implement update your lib/Stats/Basic.pm by adding the following:

=head2 mean

Returns the mean value from a list of numebrs

  my $mean = sum { $_ } 5, 1, 3, 2, 4;

=cut

sub mean (&@) {
    my ($code, @numbers) = @_;
    my $sum = sum {$code->($_)} @numbers;
    return $sum / scalar @numbers;
}
Enter fullscreen mode Exit fullscreen mode

We reuse the sum function but there is a caveat - we can't call it directly with our existing code reference. Instead, we need to create a new code reference that calls our original code reference from within. We then divide the sum by the total number of passed arguments to get the mean.

Now if you run your tests they should all pass and you now have a basic statistic module. The final task we need to complete though is the exporting of our functions so we can access them without the namespace. To do this add the following under the version declaration.

use parent 'Exporter';
our @EXPORT_OK = qw/min max sum mean/;
Enter fullscreen mode Exit fullscreen mode

And that completes this post on subroutine prototypes in Perl. Through our Stats::Basic module, we've demonstrated how prototypes can be used to validate arguments and create functions that feel like natural extensions to the Perl language, similar to built-in keywords like grep and map. In the next post, we'll explore overloading of operators, which will allow you to write custom behaviour for your objects when used with certain operators.

Top comments (1)

Collapse
 
bbkr profile image
Paweł bbkr Pabian • Edited

Note that if you use signatures the correct syntax to distinguish signature from prototype is:

use v5.40;

sub : prototype() { ... }
Enter fullscreen mode Exit fullscreen mode

TBH I don't see any reason today for choosing prototypes over signatures.