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!";
  }

(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!";
  }

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!";
  }

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

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 );

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;

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;
    }
  }

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;
    }
  }

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' },
    ],
  );

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 { ... }

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' )

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;
    }
  }

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;
    }
  }

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;

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 )

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;

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 );
  },

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;
    }
  }

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;
    }
  }

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.

Leave a comment

About Toby Inkster

user-pic I'm tobyink on CPAN, IRC and PerlMonks.