Exploring Type::Tiny Part 5: match_on_type

This is an old article. Some links may be broken. Some of the information may no longer be correct. Opinions expressed in this article may no longer be held.

Type::Tiny is probably best known as a way of having Moose-like type constraints in Moo, but it can be used for so much more. This is the fifth in a series of posts showing other things you can use Type::Tiny for. This article along with the earlier ones in the series can be found on my blog and in the Cool Uses for Perl section of PerlMonks.

It’s pretty common to do things like this:

   use Types::Standard qw( is_ArrayRef is_HashRef );
   use Carp qw( croak );
   
   sub process_data {
      my ($self, $data) = @_;
      if (is_ArrayRef($data)) {
         $self->_process_value($_) for @$data;
      }
      elsif (is_HashRef($data)) {
         $self->_process_value($_) for values %$data;
      }
      else {
         croak "Could not grok data";
      }
   }

Type::Utils provides a perhaps slightly neater way to do this:

   use Types::Standard qw( ArrayRef HashRef Any );
   use Type::Utils qw( match_on_type );
   use Carp qw( croak );
   
   sub process_data {
      my ($self, $data) = @_;
      match_on_type $data,
         ArrayRef, sub { $self->_process_value($_) for @$data },
         HashRef,  sub { $self->_process_value($_) for values %$data },
         Any,      sub { croak "Could not grok data" };
   }

The match_on_type function takes a value and a set of type–coderef pairs, dispatching to the first coderef where the value matches the type constraint. This function is stolen from Moose::Util::TypeConstraints.

You can get an order of magnitude faster though by doing something similar to what Type::Params does — compiling the match once, then calling it as needed.

Let’s look at a naïve (and wrong) way to do this first and examine the problems:

   use Types::Standard qw( ArrayRef HashRef Any );
   use Type::Utils qw( compile_match_on_type );
   use Carp qw( croak );
   
   sub process_data {
      my ($self, $data) = @_;
      state $matcher = compile_match_on_type
         ArrayRef, sub { $self->_process_value($_) for @$data },
         HashRef,  sub { $self->_process_value($_) for values %$data },
         Any,      sub { croak "Could not grok data" };
      $matcher->($data);
   }

The big problem here is that the first time process_data is called, the matcher will close over $self and $data. Subsequent calls to $matcher will reuse the same closed over variables. Oops.

The simplest way of solving this is to take advantage of the fact that a compiled matcher (unlike match_on_type) can take a list of arguments, not just one. Only the first argument is used for the type matching, but all arguments are passed to the coderefs on dispatch.

   use Types::Standard qw( ArrayRef HashRef Any );
   use Type::Utils qw( compile_match_on_type );
   use Carp qw( croak );
   
   sub process_data {
      my ($self, $data) = @_;
      state $matcher = compile_match_on_type
         ArrayRef, sub { my ($d, $s) = @_; $s->_process_value($_) for @$d },
         HashRef,  sub { my ($d, $s) = @_; $s->_process_value($_) for values %$d },
         Any,      sub { croak "Could not grok data" };
      $matcher->($data, $self);
   }

Like many Type::Tiny interfaces that expect coderefs, compile_match_on_type also accepts strings of Perl code as an alternative, and is able to optimize things better if those are supplied:

   use Types::Standard qw( ArrayRef HashRef Any );
   use Type::Utils qw( compile_match_on_type );
   use Carp qw();
   
   sub process_data {
      my ($self, $data) = @_;
      state $matcher = compile_match_on_type
         ArrayRef, q{ my ($d, $s) = @_; $s->_process_value($_) for @$d },
         HashRef,  q{ my ($d, $s) = @_; $s->_process_value($_) for values %$d },
         Any,      q{ Carp::croak("Could not grok data") };
      $matcher->($data, $self);
   }

The coderefs compiled by compile_match_on_type should be very efficient. The technique is very similar to how Type::Coercion compiles coercions.