Did you know that you could increase the readability and reliability of your Perl code with one feature? I’m talking about subroutine signatures: the ability to declare what arguments, and in some cases what types of arguments, your functions and methods take.

Most Perl programmers know about the @_ variable (or @ARG if you use English). When a subroutine is called, @_ contains the parameters passed. It’s an array (thus the @ sigil) and can be treated as such; it’s even the default argument for pop and shift. Here’s an example:

use v5.10;
use strict;
use warnings;

sub foo {
    my $parameter = shift;
    say "You passed me $parameter";
}

Or for multiple parameters:

use v5.10;
use strict;
use warnings;

sub foo {
    my ($parameter1, $parameter2) = @_;
    say "You passed me $parameter1 and $parameter2";
}

(What’s that use v5.10; doing there? It enables all features that were introduced in Perl 5.10, such as the say function. We’ll assume you type it in from now on to reduce clutter.)

We can do better, though. Perl 5.20 (released in 2014; why haven’t you upgraded?) introduced the experimental signatures feature, which as described above, allows parameters to be introduced right when you declare the subroutine. It looks like this:

use experimental 'signatures';

sub foo ($parameter1, $parameter2 = 1, @rest) {
    say "You passed me $parameter1 and $parameter2";
    say "And these:";
    say for @rest;
}

You can even set defaults for optional parameters, as seen above with the = sign, or slurp up remaining parameters into an array, like the @rest array above. For more helpful uses of this feature, consult the perlsub manual page.

We can do better still. The Comprehensive Perl Archive Network (CPAN) contains several modules that both enable signatures, as well as validate parameters are of a certain type or format. (Yes, Perl can have types!) Let’s take a tour of some of them.

Params::Validate

This module adds two new functions, validate() and validate_pos(). validate() introduces named parameters, which make your code more readable by describing what parameters are being called at the time you call them. It looks like this:

use Params::Validate;

say foo(parameter1 => 'hello',  parameter2 => 'world');

sub foo {
    my %p = validate(@_, {
        parameter1 => 1, # mandatory
        parameter2 => 0, # optional
    } );
    return $p->{parameter1}, $p->{parameter2};
}

If all you want to do is validate un-​named (positional) parameters, use validate_pos():

use Params::Validate;

say foo('hello', 'world');

sub foo {
    my @p = validate_pos(@_, 1, 0);
    return @p;
}

Params::Validate also has fairly deep support for type validation, enabling you to validate parameters against simple types, method interfaces (also known as duck typing”), membership in a class, regular expression matches, and arbitrary code callbacks. As always, consult the documentation for the nitty-​gritty details.

MooseX::Params::Validate

MooseX::Params::Validate adds type validation via the Moose object-​oriented framework’s type system, meaning that anything that can be defined as a Moose type can be used to validate the parameters passed to your functions or methods. It adds the validated_hash(), validated_list(), and pos_validated_list() functions, and looks like this:

package Foo;

use Moose;
use MooseX::Params::Validate;

say __PACKAGE__->foo(parameter1 => 'Mouse');
say __PACKAGE__->bar(parameter1 => 'Mice');
say __PACKAGE__->baz('Men', 42);

sub foo {
    my ($self, %params) = validated_hash(
        \@_,
        parameter1 => { isa => 'Str', default => 'Moose' },
    );
    return $params{parameter1};
}

sub bar {
    my ($self, $param1) = validated_pos(
        \@_,
        parameter1 => { isa => 'Str', default => 'Moose' },
    );
    return $param1;
}

sub baz {
    my ($self, $foo, $bar) = pos_validated_list(
        \@_,
        { isa => 'Str' },
        { isa => 'Int' },
    );
    return $foo, $bar;
}

Note that the first parameter passed to each function is a reference to the @_ array, denoted by a backslash.

MooseX::Params::Validate has several more things you can specify when listing parameters, including roles, coercions, and dependencies. The documentation for the module has all the details. We use this module at work a lot, and even use it without Moose when validating parameters passed to test functions.

Function::Parameters

For a different take on subroutine signatures, you can use the Function::Parameters module. Rather than providing helper functions, it defines two new Perl keywords, fun and method. It looks like this:

use Function::Parameters;

say foo('hello', 'world');
say bar(param1 => 'hello');

fun foo($param1, $param2) {
    return $param1, $param2;
}

fun bar(:$param1, :$param2 = 42) {
    return $param1, $param2;
}

The colons in the bar() function above indicate that the parameters are named, and need to be specified by name when the function is called, using the => operator as if you were specifying a hash.

In addition to defaults and the positional and named parameters demonstrated above, Function::Parameters supports type constraints (via Type::Tiny) and Moo or Moose method modifiers. (If you don’t know what those are, the Moose and Class::Method::Modifiers documentation are helpful.)

I’m not a fan of modules that add new syntax for common tasks like subroutines and methods, if only because there’s an extra effort in updating toolings like syntax highlighters and Perl::Critic code analysis. Still, this may appeal to you, especially if you’re coming from other languages that have similar syntax.

Type::Params

Speaking of Type::Tiny, it includes its own parameter validation library called Type::Params. I think I would favor this for new work, as it’s compatible with both Moo and Moose but doesn’t require them.

Type::Params has a number of functions, none of which are provided by default, so you’ll have to import them explicitly when use ing the module. It also introduces a separate step for compiling your validation specification to speed up performance. It looks like this:

use Types::Standard qw(Str Int);
use Type::Params qw(compile compile_named);

say foo('hello', 42);
say bar(param1 => 'hello');

sub foo {
    state $check = compile(Str, Int);
    my ($param1, $param2) = $check->(@_);

    return $param1, $param2;
}

sub bar {
    state $check = compile_named(
        param1 => Str,
        param2 => Int, {optional => 1},
    );
    my $params_ref = $check->(@_);

    return $params_ref->{param1}, $params_ref->{param2};
}

The features of Type::Tiny and its bundled modules are pretty vast, so I suggest once again that you consult the documentation on how to use it.

Params::ValidationCompiler

At the top of the documentation to Params::Validate, you’ll notice that the author recommends instead his Params::ValidationCompiler module for faster performance, using a compilation step much like Type::Params. It provides two functions for you to import, validation_for() and source_for(). We’ll concentrate on the former since the latter is mainly useful for debugging.

It looks like this:

use Types::Standard qw(Int Str);
use Params::ValidationCompiler 'validation_for';

my $validator = validation_for(
    params => {
        param1 => {
            type    => Str,
            default => 'Perl is cool',
        },
        param2 => {
            type     => Int,
            optional => 1,
        },
);

say foo(param1 => 'hello');

sub foo {
    my %params = $validator->(@_);
    return @params{'param1', 'param2'};
}

As you can see, it supports type constraints, defaults, and optional values. It can also put extra arguments in a list (it calls this feature slurpy”), and can even return generated objects to make it easier to catch typos (since a typoed hash key just generates that key rather than returning an error). There’s a bit more to this module, so please read the documentation to examine all its features.

Conclusion

One of Perl’s mottos is there’s more than one way to do it,” and you’re welcome to choose whatever method you need to enable signatures and type validation. Just remember to be consistent and have good reasons for your choices, since the overall goal is to improve your code’s reliability and readability. And be sure to share your favorite techniques with others, so they too can develop better software.

This article is also published on dev.to.

5 thoughts on “Better Perl with subroutine signatures and type validation

Comments are closed.