Using Type::Params Effectively
Tags: perl, typetiny
Published: 2023-02-17 16:54:39

---
title: Using Type::Params Effectively
published: true
date: 2023-02-17 16:48:37 UTC
tags: Perl,perl,TypeTiny
canonical_url: https://toby.ink/blog/2023/02/17/using-typeparams-effectively/
---

One of the modules bundled with [Type::Tiny](https://metacpan.org/pod/Type%3A%3ATiny) is [Type::Params](https://metacpan.org/pod/Type%3A%3AParams), 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:

```perl
    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.

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

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

```perl
    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.

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

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

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

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

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

```perl
    default => sub { ... }
```

## Switching to named parameters

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

```perl
    $point->move_to( x => 1, y => 2, reason => 'whatever' )
```

Type::Params handles named parameters easily.

```perl
    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!

```perl
    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.

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

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

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

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

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

```perl
    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.