DEV Community

Nicholas Hubbard
Nicholas Hubbard

Posted on • Edited on

Make Perl Count Arguments

Unlike many programming languages, Perl doesn't check that functions are called with the correct number of arguments. This is because Perl subroutines are variadic by default, which makes a lot of programming tasks really easy. The downside is that a lot of the time functions only make sense if they are called with a certain number of arguments.

Most of the time if a subroutine is called with an incorrect number of arguments it is because the programmer made a simple typo. To make our lives as programmers easier, it would be nice if we could detect this situation and give a clear error message about what went wrong.

We will explore a solution to this problem.

WARNING: The examples in this artical call Perl from the command line. If you don't understand Perl command line syntax, only pay attention to what comes after the -E.

Lets look at an example.

package T;

sub num_diff {
    my $n1 = shift;
    my $n2 = shift;
    return abs($n1 - $n2);
}

1;
Enter fullscreen mode Exit fullscreen mode

When called with two arguments our function behaves as expected.

$ perl -W -I. -MT -E 'say T::num_diff(7, 17)'
10
Enter fullscreen mode Exit fullscreen mode

But what if we call num_diff with more than two arguments?

$ perl -W -I. -MT -E 'say T::num_diff(23, 21, 48)'
2
Enter fullscreen mode Exit fullscreen mode

Perl has no issue if we call this function with 3 arguments, and happily returns us the difference between the first two arguments. This is bad! The difference between 3 numbers is certainly not the difference between the first 2.

A nice way to deal with this problem is to use signatures, which provides syntax for declaring a subroutines arguments. Lets rewrite num_diff using signatures.

package T;

use v5.20;
use feature 'signatures';

sub num_diff($n1, $n2) {
    return abs($n1 - $n2);
}

1;
Enter fullscreen mode Exit fullscreen mode

Lets see what happens when we call num_diff with more than 2 arguments.

$ perl -I. -MT -E 'say T::num_diff(22, 33, 8)'
Too many arguments for subroutine 'T::num_diff' (got 3; expected 2) at -e line 1.
Enter fullscreen mode Exit fullscreen mode

Awesome, our problem is solved! With signatures Perl can count our subroutine arguments and give us error diagnositics when we mess up.

Unfortunately though there are some downsides to signatures. First off signatures didn't exist until Perl version 5.20, so if you're use an old Perl signatures are not an option. The other downside is that signatures were experimental until Perl version 5.36, which is why the use feature 'signatures' statement is necessary.

I have been working on a Perl project that uses Perl version 5.16.3, so I cannot use signatures. To count arguments I wrote a function that I call as the first statement in subroutines that kills the program if it did not receive the correct number of arguments.

use Carp 'confess';

sub arg_count_or_die {

    # Carp::Confess unless $num_args is in range $lower-$upper

    my $lower    = shift;
    my $upper    = shift;
    my $num_args = @_;

    ($lower, $upper) = ($upper, $lower) if $lower > $upper;

    unless ($lower <= $num_args && $num_args <= $upper) {
        my $caller = ( caller(1) )[3];
        my $expected_plural = $lower == 1 ? '': 's';
        my $got_plural = $num_args == 1 ? '' : 's';
        my $arg_range_msg = $lower == $upper ? "$lower arg$expected_plural" : "$lower-$upper args";
        confess("yabsm: internal error: called '$caller' with $num_args arg$got_plural but it expects $arg_range_msg");
    }


    return 1;
}
Enter fullscreen mode Exit fullscreen mode

Lets rewrite num_diff to use this function.

sub num_diff {

    arg_count_or_die(2, 2, @_);

    my $n1 = shift;
    my $n2 = shift;

    return abs($n1 - $n2);
}
Enter fullscreen mode Exit fullscreen mode

Again, lets call num_diff with more than 2 arguments

$ perl -I. -MT -E 'say T::num_diff(22, 33, 8)'
my-program: internal error: call to 'T::num_diff' passed 3 args but expects 2 args at T.pm line 19.
        T::arg_count_or_die(2, 2, 22, 33, 8) called at T.pm line 27
        T::num_diff(22, 33, 8) called at -e line 1
Enter fullscreen mode Exit fullscreen mode

We can see that by using Carp::Confess we get an excellent error message that shows the call stack that lead to the to our erroneus subroutine call. By prefixing the error message with my-program: internal error, if this error ever occurs our user knows that they found a bug and can send us the stack trace which will be very useful for debugging.

Top comments (0)