Better Perl with subroutine signatures and type validation
Tags: perl, programming, signatures, types
Published: 2021-01-27 22:30:47

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 [`@_`](https://perldoc.pl/perlvar#@_) variable (or `@ARG` if you [`use English`](https://perldoc.pl/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`](https://perldoc.pl/functions/pop) and [`shift`](https://perldoc.pl/functions/shift). Here's an example:

```perl
use v5.10;
use strict;
use warnings;

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

Or for multiple parameters:

```perl
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](https://perldoc.pl/perl5100delta), such as the [`say`](https://perldoc.pl/perlfunc#say) function. We'll assume you type it in from now on to reduce clutter.)

**We can do better**, though. [Perl 5.20](https://perldoc.pl/perl5200delta) (released in 2014; why haven't you upgraded?) introduced the experimental [`signatures`](https://perldoc.pl/perlsub#Signatures) feature, which as described above, allows parameters to be introduced right when you declare the subroutine. It looks like this:

```perl
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](https://perldoc.pl/perlsub#Signatures) manual page.

**We can do better still**. The [Comprehensive Perl Archive Network (CPAN)](https://www.cpan.org/) 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](https://metacpan.org/pod/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:

```perl
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()`:

```perl
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](https://metacpan.org/pod/Params::Validate#Type-Validation), [method interfaces](https://metacpan.org/pod/Params::Validate#Interface-Validation) (also known as "duck typing"), [membership in a class](https://metacpan.org/pod/Params::Validate#Class-Validation), [regular expression matches](https://metacpan.org/pod/Params::Validate#Regex-Validation), and [arbitrary code callbacks](https://metacpan.org/pod/Params::Validate#Callback-Validation). As always, consult the [documentation](https://metacpan.org/pod/Params::Validate) for the nitty-gritty details.

## [MooseX::Params::Validate](https://metacpan.org/pod/MooseX::Params::Validate)

MooseX::Params::Validate adds type validation via the [Moose](https://metacpan.org/pod/Moose) object-oriented framework's type system, meaning that anything that can be defined as a [Moose type](https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/Types.pod) can be used to validate the parameters passed to your functions or methods. It adds the [`validated_hash()`](https://metacpan.org/pod/MooseX::Params::Validate#validated_hash(-\\@_,-\%parameter_spec-)), [`validated_list()`](https://metacpan.org/pod/MooseX::Params::Validate#validated_list(-\\@_,-\%parameter_spec-)), and [`pos_validated_list()`](https://metacpan.org/pod/MooseX::Params::Validate#pos_validated_list(-\\@_,-$spec,-$spec,-...-)) functions, and looks like this:

```perl
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](https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/Roles.pod), [coercions](https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/Types.pod#COERCION), and dependencies. The [documentation for the module](https://metacpan.org/pod/MooseX::Params::Validate) 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](https://metacpan.org/pod/Function::Parameters)

For a different take on subroutine signatures, you can use the [Function::Parameters](https://metacpan.org/pod/Function::Parameters) module. Rather than providing helper functions, it defines two new Perl keywords, `fun` and `method`. It looks like this:

```perl
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](https://metacpan.org/pod/Function::Parameters#Default-arguments) and the [positional](https://metacpan.org/pod/Function::Parameters#Simple-parameter-lists) and [named](https://metacpan.org/pod/Function::Parameters#Named-parameters) parameters demonstrated above, Function::Parameters supports [type constraints](https://metacpan.org/pod/Function::Parameters#Type-constraints) (via [Type::Tiny](https://metacpan.org/pod/Type::Tiny)) and Moo or Moose [method modifiers](https://metacpan.org/pod/Function::Parameters#Method-modifiers). (If you don't know what those are, the [Moose](https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/MethodModifiers.pod) and [Class::Method::Modifiers](https://metacpan.org/pod/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](http://perlcritic.com/) code analysis. Still, this may appeal to you, especially if you're coming from other languages that have similar syntax.

## [Type::Params](https://metacpan.org/pod/Type::Params)

Speaking of [Type::Tiny](https://metacpan.org/pod/Type::Tiny), it includes its own parameter validation library called [Type::Params](https://metacpan.org/pod/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:

```perl
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](https://metacpan.org/pod/Type::Tiny::Manual) on how to use it.

## [Params::ValidationCompiler](https://metacpan.org/pod/Params::ValidationCompiler)

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

It looks like this:

```perl
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](https://metacpan.org/pod/Params::ValidationCompiler) 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.