Better Perl with subroutine signatures and type validation

Did you know that you could increase the read­abil­i­ty and reli­a­bil­i­ty of your Perl code with one fea­ture? I’m talk­ing about sub­rou­tine sig­na­tures: the abil­i­ty to declare what argu­ments, and in some cas­es what types of argu­ments, your func­tions and meth­ods take.

Most Perl pro­gram­mers know about the @_ vari­able (or @ARG if you use English). When a sub­rou­tine is called, @_ con­tains the para­me­ters passed. It’s an array (thus the @ sig­il) and can be treat­ed as such; it’s even the default argu­ment 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 mul­ti­ple 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 fea­tures that were intro­duced in Perl 5.10, such as the say func­tion. We’ll assume you type it in from now on to reduce clutter.)

We can do bet­ter, though. Perl 5.20 (released in 2014; why haven’t you upgrad­ed?) intro­duced the exper­i­men­tal signatures fea­ture, which as described above, allows para­me­ters to be intro­duced right when you declare the sub­rou­tine. 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 option­al para­me­ters, as seen above with the = sign, or slurp up remain­ing para­me­ters into an array, like the @rest array above. For more help­ful uses of this fea­ture, con­sult the perl­sub man­u­al page.

We can do bet­ter still. The Comprehensive Perl Archive Network (CPAN) con­tains sev­er­al mod­ules that both enable sig­na­tures, as well as val­i­date para­me­ters are of a cer­tain type or for­mat. (Yes, Perl can have types!) Let’s take a tour of some of them.

Params::Validate

This mod­ule adds two new func­tions, validate() and validate_pos(). validate() intro­duces named para­me­ters, which make your code more read­able by describ­ing what para­me­ters 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 val­i­date un-​named (posi­tion­al) para­me­ters, use validate_pos():

use Params::Validate;

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

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

Params::Validate also has fair­ly deep sup­port for type val­i­da­tion, enabling you to val­i­date para­me­ters against sim­ple types, method inter­faces (also known as duck typ­ing”), mem­ber­ship in a class, reg­u­lar expres­sion match­es, and arbi­trary code call­backs. As always, con­sult the doc­u­men­ta­tion for the nitty-​gritty details.

MooseX::Params::Validate

MooseX::Params::Validate adds type val­i­da­tion via the Moose object-​oriented frame­work’s type sys­tem, mean­ing that any­thing that can be defined as a Moose type can be used to val­i­date the para­me­ters passed to your func­tions or meth­ods. It adds the validated_hash(), validated_list(), and pos_validated_list() func­tions, 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 para­me­ter passed to each func­tion is a ref­er­ence to the @_ array, denot­ed by a backslash.

MooseX::Params::Validate has sev­er­al more things you can spec­i­fy when list­ing para­me­ters, includ­ing roles, coer­cions, and depen­den­cies. The doc­u­men­ta­tion for the mod­ule has all the details. We use this mod­ule at work a lot, and even use it with­out Moose when val­i­dat­ing para­me­ters passed to test functions.

Function::Parameters

For a dif­fer­ent take on sub­rou­tine sig­na­tures, you can use the Function::Parameters mod­ule. Rather than pro­vid­ing helper func­tions, it defines two new Perl key­words, 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() func­tion above indi­cate that the para­me­ters are named, and need to be spec­i­fied by name when the func­tion is called, using the => oper­a­tor as if you were spec­i­fy­ing a hash.

In addi­tion to defaults and the posi­tion­al and named para­me­ters demon­strat­ed above, Function::Parameters sup­ports type con­straints (via Type::Tiny) and Moo or Moose method mod­i­fiers. (If you don’t know what those are, the Moose and Class::Method::Modifiers doc­u­men­ta­tion are helpful.)

I’m not a fan of mod­ules that add new syn­tax for com­mon tasks like sub­rou­tines and meth­ods, if only because there’s an extra effort in updat­ing tool­ings like syn­tax high­lighters and Perl::Critic code analy­sis. Still, this may appeal to you, espe­cial­ly if you’re com­ing from oth­er lan­guages that have sim­i­lar syntax.

Type::Params

Speaking of Type::Tiny, it includes its own para­me­ter val­i­da­tion library called Type::Params. I think I would favor this for new work, as it’s com­pat­i­ble with both Moo and Moose but does­n’t require them.

Type::Params has a num­ber of func­tions, none of which are pro­vid­ed by default, so you’ll have to import them explic­it­ly when use ing the mod­ule. It also intro­duces a sep­a­rate step for com­pil­ing your val­i­da­tion spec­i­fi­ca­tion to speed up per­for­mance. 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 fea­tures of Type::Tiny and its bun­dled mod­ules are pret­ty vast, so I sug­gest once again that you con­sult the doc­u­men­ta­tion on how to use it.

Params::ValidationCompiler

At the top of the doc­u­men­ta­tion to Params::Validate, you’ll notice that the author rec­om­mends instead his Params::ValidationCompiler mod­ule for faster per­for­mance, using a com­pi­la­tion step much like Type::Params. It pro­vides two func­tions for you to import, validation_for() and source_for(). We’ll con­cen­trate on the for­mer since the lat­ter is main­ly use­ful 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 sup­ports type con­straints, defaults, and option­al val­ues. It can also put extra argu­ments in a list (it calls this fea­ture slur­py”), and can even return gen­er­at­ed objects to make it eas­i­er to catch typos (since a typoed hash key just gen­er­ates that key rather than return­ing an error). There’s a bit more to this mod­ule, so please read the doc­u­men­ta­tion to exam­ine all its features.

Conclusion

One of Perl’s mot­tos is there’s more than one way to do it,” and you’re wel­come to choose what­ev­er method you need to enable sig­na­tures and type val­i­da­tion. Just remem­ber to be con­sis­tent and have good rea­sons for your choic­es, since the over­all goal is to improve your code’s reli­a­bil­i­ty and read­abil­i­ty. And be sure to share your favorite tech­niques with oth­ers, so they too can devel­op bet­ter software.

This arti­cle is also pub­lished on dev.to.


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Comments

5 responses to “Better Perl with subroutine signatures and type validation”

  1. […] I use this a lot when assign­ing argu­ments received from func­tions or meth­ods (see my pre­vi­ous arti­cle on sub­rou­tine signatures): […]

  2. […] Bet­ter Perl: Sub­rou­tine Sig­na­tures and Type Val­i­da­tion” (based on my ear­li­er blog post) for Hous­ton Perl Mon­gers on Thurs­day, Feb­ru­ary 11 at 6:00 PM Central […]

  3. […] the heels of my blog arti­cle and upcom­ing pre­sen­ta­tion comes Paul Evans’ call to de-​exper­i­men­tal­ize (is that a […]

  4. […] few weeks ago I wrote an arti­cle cov­er­ing six ways to do sub­rou­tine sig­na­tures and type con­straints in Perl. Some of the feed­back I got indi­cat­ed that there were more options to con­sid­er, so for […]

  5. […] The orig­i­nal arti­cle that inspired this pre­sen­ta­tion is here. […]

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)