Type::Tiny Tricks #7: Tricks with Dicts

In Perl, hashes are typically used for two sorts of purposes: maps (where the hash key acts as an object identifier) and dictionaries (where the hash key acts like a field name). A quick illustration of what I mean by this:

   # Maps
   my %ages = (
      alice   => 24,
      bob     => 25,
      carol   => 31,
   );
   my %email = (
      alice   => "alice@example.net",
      bob     => "robert.smith@example.com",
      carol   => "c_jones@example.org",
   );
   
   # Dictionaries
   my %alice = ( age => 24, email => "alice@example.net" );
   my %bob   = ( age => 25, email => "robert.smith@example.com" );
   my %carol = ( age => 32, email => "c_jones@example.org" );

These are two different styles of using hashes. Sometimes one is useful, and sometimes the other is. Sometimes neither is better and your choice of one over the other is fairly arbitrary.

Types::Standard contains a type constraint called Map which is useful for validating references to the first kind of hash. (I copied the idea from MooseX::Types::Structured.) But today I'm mainly going to talk about another type constraint: Dict. (Yeah, I copied that one too.)

Let's say you have a value which is supposed to be hashref pointing to a hash along the lines of %alice or %bob above. We can define a type constraint like this:

   use Types::Standard qw( Dict Int );
   use Type::EmailAddress qw( EmailAddress );
   
   my $type = Dict[ age => Int, email => EmailAddress ];

And we can check a hashref against the type constraint like this:

   $type->check($href) or die "Argghh!!";

Or maybe you can't think of a funny message to die with, so instead you can rely on Type::Tiny to generate one for you.

   $type->assert_valid($href);   # dies if invalid

Now that's useful, but perhaps sometimes we don't know a person's age. In this case, we can make it optional:

   $type = Dict[ age => Optional[Int], email => EmailAddress ];

A hashref with a missing age key will now pass the type constraint, though a hashref with an explicit undef in the age slot will fail. Maybe you want to indicate an unknown age with an explicit undef though. So then you'd use:

   $type = Dict[ age => Int|Undef, email => EmailAddress ];

Or perhaps either way is OK. If you don't know the age, sometime you'll provide an explicit undef, and sometimes you'll just leave the age key out of the hashref. OK, that's fine too...

   $type = Dict[ age => Optional[Int|Undef], email => EmailAddress ];

Here's our last trick. Currently, the following hashref will fail the type constraint check:

   $href = {
      age        => 30,
      email      => "dave@example.net",
      x_website  => "http://example.net/~dave/",
   };

Extra keys are not allowed. This limits the extensibility of our code. But if you read my previous article about tricks with tuples, then you might have an idea about the solution.

   use Types::Standard qw( Dict Optional Int slurpy Map StrMatch Str );
   use Type::EmailAddress qw( EmailAddress );
   
   my $type = Dict[
      age   => Optional[Int],
      email => EmailAddress,
      slurpy Map[StrMatch[qr/^x_/], Str],
   ];

OK, this probably requires some explanation. This type constraint will validate age and email as usual, but then if the hashref contains any unrecognized fields, these will be slurped up into a temporary hashref (let's call it \%tmp). It then validates \%tmp against the Map[StrMatch[qr/^x_/], Str] type constraint.

What is Map[StrMatch[qr/^x_/], Str]? It's a hashref where all keys are strings matching qr/^x_/, and all values are strings. So our hashref of data about Dave should pass OK.

Pop quiz! What does this mean?

   $type = Dict[
      age   => Optional[Int],
      email => EmailAddress,
      slurpy Any,
   ];

Answer: it validates the age and email as usual, and slurps any remaining keys into \%tmp, and validates that against the Any type constraint.

(In reality this is more optimized. The Any type constraint always passes, no matter what value you check. So Type::Tiny is smart enough to never actually create that \%tmp hash.)

The combination of Dict, Tuple, Map, HashRef, and ArrayRef allows for some very precise deep validation of values, but remember not to get carried away. A simple:

   isa => HashRef

... will run much faster if you're not in need of all that fancy validation!

Leave a comment

About Toby Inkster

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