DEV Community

Toby Inkster
Toby Inkster

Posted on • Originally published at toby.ink on

Using Type::Params Effectively

One of the modules bundled with Type::Tiny is Type::Params, a module
that allows you to validate subroutine signatures using type constraints.
It's one of the more popular parts of the suite.

This article provides a few hints for using it effectively.

When not to use Type::Params

There are a few places I occasionally see Type::Params being used where
it's really not the best fit.

You just want to know if a variable meets a type constraint

Let's say you have a variable $x and you want to know if it's
an integer. I have seen people do things like this:

    use v5.16;
    use Types::Standard qw( Int );
    use Type::Params qw( validate );

    if ( eval { validate [ $x ], Int } ) {
      say "x is an integer!";
    }
Enter fullscreen mode Exit fullscreen mode

(The eval is used because validate will throw an exception, when what
we want is a boolean yes/no.)

This works, yes, but it can be made so much simpler!

The Int() function returns an Int type constraint object which we can
call methods on. In particular, the check method.

    use v5.16;
    use Types::Standard qw( Int );

    if ( Int->check( $x ) ) {
      say "x is an integer!";
    }
Enter fullscreen mode Exit fullscreen mode

Because type libraries also export an is_X() function for each type
constraint, we can even go one step simpler:

    use v5.16;
    use Types::Standard qw( is_Int );

    if ( is_Int $x ) {
      say "x is an integer!";
    }
Enter fullscreen mode Exit fullscreen mode

This is not just easier to read; it will also benchmark much, much faster.

You want to assert that a variable meets a type constraint

This is a similar situation, except that here we want an exception to be
thrown.

I see people do things like this:

    use v5.16;
    use Types::Standard qw( Int );
    use Type::Params qw( compile );

    state $check_int = compile( Int );
    $check_int->( $x );                 # assert $x is an integer
Enter fullscreen mode Exit fullscreen mode

Again, we can call methods on the Int type constraint to achieve the
same result.

    use v5.16;
    use Types::Standard qw( Int );

    Int->assert_valid( $x );
Enter fullscreen mode Exit fullscreen mode

Or we can use the assert_X() functions exported by type constraint
libraries:

    use v5.16;
    use Types::Standard qw( assert_Int );

    assert_Int $x;
Enter fullscreen mode Exit fullscreen mode

Once again, this is not only much clearer, but will perform better in
benchmarks.

When to use Type::Params

Type::Params is intended for validating and unpacking an entire list
in one logical step. In particular this is useful for validating the
@_ array of subroutine arguments.

Consider we have a class something like this:

    use v5.16;

    package Point {
      use Moo;
      use Types::Common qw( Num );

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      sub move_to {
        my ( $self, $new_x, $new_y, $reason ) = @_;
        $self->x( $new_x );
        $self->y( $new_y );
        say $reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

This looks safe because the type constraints will ensure that x and y
will always be numeric.

However, what if somebody does $point->move_to( 1, '', 'whatever' )?
Setting y to the empty string will throw an exception, but x has
already been changed, so the object is left in an inconsistent state. It
hasn't moved to where we wanted it to move, but it hasn't stayed where
it was either!

So it's a good idea to validate all the parameters to move_to up front.
Here's how you could do it using Type::Params:

    use v5.16;

    package Point {
      use Moo;
      use Types::Common -types, -sigs;

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      sub move_to {
        state $check = signature(
          method     => Object,
          positional => [ Num, Num, Str ],
        );
        my ( $self, $new_x, $new_y, $reason ) = &$check;
        $self->x( $new_x );
        $self->y( $new_y );
        say $reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

The signature function compiles the function's signature into a coderef
called $check. This is stored in a state variable so that it is
compiled only once.

The method option indicates that this sub will be called as a method,
so will have an invocant (which must be an Object). The positional
option indicates that this sub expects two numbers followed by a string.

Calling &$check with no parentheses takes advantage of the Perl
feature where @_ is automatically passed forward. The $check
coderef will now validate @_ and return the validated values. If
validation fails, it will throw an exception.

Adding defaults

Type::Params also supports defaults:

    state $check = signature(
      method     => Object,
      positional => [
        Num,
        Num,
        Str, { default => 'cos I want to' },
      ],
    );
Enter fullscreen mode Exit fullscreen mode

Now if the function is called without a $reason, a default reason
can be supplied. Simple non-reference defaults can be provided as above.
If you need to build a more complex default, you can use:

    default => sub { ... }
Enter fullscreen mode Exit fullscreen mode

Switching to named parameters

Let's assume we would prefer to call the method like this:

    $point->move_to( x => 1, y => 2, reason => 'whatever' )
Enter fullscreen mode Exit fullscreen mode

Type::Params handles named parameters easily.

    use v5.16;

    package Point {
      use Moo;
      use Types::Common -types, -sigs;

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      sub move_to {
        state $check = signature(
          method => Object,
          named  => [
            x      => Num,
            y      => Num,
            reason => Str, { default => 'cos I want to' },
          ],
        );
        my ( $self, $arg ) = &$check;
        $self->x( $arg->x );
        $self->y( $arg->y );
        say $arg->reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

The $check coderef will now return just the invocant $self
and an object $arg which allows access to the named arguments.

However, perhaps we want to accept named parameters without rewriting
the guts of move_to to deal with $arg. We still want our
$new_x, $new_y, and $reason variables. That is
also possible!

    use v5.16;

    package Point {
      use Moo;
      use Types::Common -types, -sigs;

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      sub move_to {
        state $check = signature(
          method        => Object,
          named_to_list => 1,
          named         => [
            x      => Num,
            y      => Num,
            reason => Str, { default => 'cos I want to' },
          ],
        );
        my ( $self, $new_x, $new_y, $reason ) = &$check;
        $self->x( $new_x );
        $self->y( $new_y );
        say $reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

The named_to_list option tells $check that it needs to accept
named parameters, but return them in a positional list.

Transitioning

Now we have rewritten move_to to accept named parameters, but perhaps
there are still places all over our codebase that call move_to with
positional parameters. How can we accept both?

Type::Params allows multiple signatures to be combined into one using the
multiple option.

    state $check = signature(
      method   => Object,
      multiple => [
        {
          named_to_list => 1,
          named         => [
            x      => Num,
            y      => Num,
            reason => Str, { default => 'cos I want to' },
          ],
        },
        {
          positional => [
            Num,
            Num,
            Str, { default => 'cos I want to' },
          ],
        },
      ],
    );
    my ( $self, $new_x, $new_y, $reason ) = &$check;
Enter fullscreen mode Exit fullscreen mode

Now $check will try each signature in order and see which seems to
make sense. move_to will accept both named and positional arguments.

Extra flexibility

For some reason, we now also want to allow people to call:

    my @coordinates = ( $x, $y );
    $point->move_to( \@coordinates, $reason )
Enter fullscreen mode Exit fullscreen mode

And still preserve the two existing ways to call the function.
Type::Params can still handle this situation. Firstly, we add
another option to the multiple list:

    state $check = signature(
      method   => Object,
      multiple => [
        {
          named_to_list => 1,
          named         => [
            x      => Num,
            y      => Num,
            reason => Str, { default => 'cos I want to' },
          ],
        },
        {
          positional => [
            Num,
            Num,
            Str, { default => 'cos I want to' },
          ],
        },
        {
          positional => [
            Tuple[ Num, Num ],
            Str, { default => 'cos I want to' },
          ],
          goto_next => sub { ... },
        },
      ],
    );
    my ( $self, $new_x, $new_y, $reason ) = &$check;
Enter fullscreen mode Exit fullscreen mode

An issue with this new calling style is that instead of returning
four arguments (the invocant, two numbers, and the reason), it returns
three arguments (the invocant, an arrayref, and the reason). But we can
use goto_next to unpack the arrayref:

    goto_next => sub {
      my ( $self, $pair, $reason ) = @_;
      return ( $self, $pair->[0], $pair->[1], $reason );
    },
Enter fullscreen mode Exit fullscreen mode

And now it all works!

Perl 5.20 subroutine signatures

Perl 5.20 introduced subroutine signatures as an experimental feature.
As of Perl 5.36, the feature is no longer experimental.

At first glance, this doesn't seem to fit well with Type::Params.
Consider:

    use v5.16;

    package Point {
      use Moo;
      use Types::Common -types, -sigs;

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      sub move_to ( $self, $new_x, $new_y, $reason ) {
        state $check = signature(
          method     => Object,
          positional => [ Num, Num, Str ],
        );
        ( $self, $new_x, $new_y, $reason )
          = $check->( $self, $new_x, $new_y, $reason );
        $self->x( $new_x );
        $self->y( $new_y );
        say $reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

It's... not neat. And we've lost the flexibility to support multiple
different calling styles.

However, Type::Params also provides a signature_for function which
inverts its usual declaration style, putting the signature check
outside the sub, and allowing you to use Perl subroutine signatures
for the sub itself.

Here's how we'd use it with named + positional calling style:

    use v5.16;

    package Point {
      use Moo;
      use Types::Common -types, -sigs;

      has x => ( is => 'rw', isa => Num, default => 0 );
      has y => ( is => 'rw', isa => Num, default => 0 );

      signature_for move_to => (
        method   => Object,
        multiple => [
          {
            named_to_list => 1,
            named         => [
              x      => Num,
              y      => Num,
              reason => Str, { default => 'cos I want to' },
            ],
          },
          {
            positional => [
              Num,
              Num,
              Str, { default => 'cos I want to' },
            ],
          },
        ],
      );

      sub move_to ( $self, $new_x, $new_y, $reason ) {
        $self->x( $new_x );
        $self->y( $new_y );
        say $reason;
        return $self;
      }
    }
Enter fullscreen mode Exit fullscreen mode

The signature_for keyword operates at run-time, wrapping around
the move_to sub declared below it. The signature check takes care
of ensuring the parameters are always sent as a list, so the Perl
subroutine signature never needs to deal with named arguments.

Currently goto_next is not supported by signature_for, so the
third calling style we had before will not work as expected. However,
support is planned.

Hopefully this article has given you an idea of what Type::Params
is capable of, how you can get the most out of it, and when it's better
to use something else.

Top comments (0)