DEV Community

loading...

Better Perl with subroutine signatures and type validation

mjgardner profile image Mark Gardner Originally published at phoenixtrap.com Updated on ・6 min read

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";
}
Enter fullscreen mode Exit fullscreen mode

Or for multiple parameters:

use v5.10;
use strict;
use warnings;

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

(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;
}
Enter fullscreen mode Exit fullscreen mode

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};
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 useing 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};
}
Enter fullscreen mode Exit fullscreen mode

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'};
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (5)

Collapse
thibaultduponchelle profile image
Tib

Thank you a lot for this blog post, it sounds like a reference post on the topic for me. Signatures/prototypes are an area not clear for me (and probably many others) in Perl (I mentioned my thoughts on this topic in Five (syntax) things that I would like to change in Perl )

While I'm here, you could colorize your code snippets with by adding "perl" after the 3 backticks ` when you open a block of code.

Collapse
mjgardner profile image
Mark Gardner Author

If you’re interested, I’ll be giving an expanded presentation on this topic at Houston Perl Mongers next week. It’s a Zoom meeting Thursday, February 11 at 6 PM Central US time. Check this link for the details, including the perl command needed to obtain the password.

Collapse
mjgardner profile image
Mark Gardner Author • Edited

Yah, in general, I would avoid prototypes. Per Damian Conway's Perl Best Practices, they're a bit difficult in that they will turn arrays that you think will be treated like individual list items into being evaluated in scalar context, thus only passing the number of elements in the array. They're a bit too magical.

Thanks for the tip about syntax highlighting! I'm new to publishing here, so everything helps!

Collapse
jsf116 profile image
jsf116 • Edited

Thank you for this really interesting article and analysis therein!
However, I am a little bit surprised that you paste a code that cannot be executed.
For instance, the very last example concerning Params::ValidationCompiler cannot work because the declaration of $validator is after the call of foo using this variable.

Collapse
mjgardner profile image
Mark Gardner Author

You’re right! Thanks, I’ve fixed it.

Forem Open with the Forem app