Monday, August 27, 2007

Perl parameter validation and error handling

One of Perl’s strengths is that “There’s more than one way to do it” (TMTOWTDI, often pronounced tim-toady). This makes for a language that is flexible, and allows you to write a solution to a problem that matches the problem, rather than trying to shoehorn the problem in to the language in order to solve it.

This is also one of Perl’s weaknesses. There can be many ways to solve a particular problem — some of them are not optimal.

One of those, that I’ve rolled countless home-grown solutions for over the years, is that of parameter validation and handling. When writing code (especially library code that’s probably going to be called by code other than your own) it pays to be defensive. Catch and report errors as soon as possible and you can save yourself (and quite possibly someone else) a boatload of debugging time.

Perl’s parameter passing is very flexible, adhering to the TMTOWTDI principle. Which also means there are lots of ways to carry out parameter validation. Here’s the approach that I’ve settled on. I hope you find it useful too.

I’m lazy. I want to hand as much of the work off to someone else’s code as possible. To that end I make use of two other Perl modules. Params::Validate and Exception::Class. Together, these two can do all the heavy lifting of detecting errors in the parameter list and reporting them in a coherent manner to the caller.

Consider a hypothetical module, called Example::Module. This module provides one function, which accepts a hashref, the keys of which specify parameter names, the values of which are the parameter values.

Here’s how I’d start that module.

  1 package Example::Module
2
3 use strict;
4 use warnings
5
6 use Params::Validate qw(:all);
7 use Exception::Class(
8 'Example::Module::X::Args'
9 => { alias => 'throw_args', },
10 );
11
12 Params::Validate::validation_options(
13 on_fail => sub { throw_args(error => shift) },
14 );

The first four lines declare the module and enable’s Perl’s strictness checks and related warnings. These are more or less boilerplate, and should be a standard part of any code written in Perl (with, perhaps, the exception of one liners written on the command line).

Line 6 loads Params::Validate, and imports the functions and constants that it defines in to the caller’s (Example::Module) namespace.

Lines 7 through 10 are more interesting, doing a number of things.

The Exception::Class module is loaded. It’s import() routine is called, and told to create a new package, Example::Module::X::Args. I use this convention frequently now — exceptions are created in namespaces based on the main namespace, with an appended “::X”, followed by the type of exception. In this case these exceptions relate to problems with the arguments passed to functions. The alias directive creates a new function, called throw_args(). If called, this function will throw an exception of the type Example::Module::X::Args, passing along any parameters it’s given.

Lines 12 through 13 make use of this. Now that throw_args() has been created, Params::Validate::validation_options() is called. This provides an interface in to the inner workings of Params::Validate. This stanza configures Params::Validate to call the newly created throw_args() function is parameter validation fails. The error => shift code ensures that any error message created by Params::Validate is included in the exception.

That’s all that you need to do in terms of set up. Now you just need to make sure that any function that requires parameter validation uses the Params::Validate functions appropriately.

Suppose we have one function in this module, frob(), that takes a hash ref of arguments:

  1 sub frob {
2 my $args = validate(@_, {
3 foo = 1,
4 });
5
6 return 1;
7 }

That’s enough to declare that foo is mandatory parameter. Now if you try and call that function without passing in that parameter your application dies with an appropriate message.

Example::Module::frob();

# Dies, printing "Mandatory parameter 'foo' missing in call to Example::Module::frob"

Example::Module::frob(foo => 1); # works

Exception::Class has other tricks up its sleeve. For example, instead of dying with a single error message you can have it generate a stack trace any time an exception is raised. By inserting this line before the call to validation_options:

Example::Module::X::Args->Trace(1)

this tracing facility is enabled.

Exception::Class can be used to easily throw other errors as well — errors that it might make more sense to trap. Consider an application that retrieves and processes data from a database. Your Exception::Class import might look like this:

use Exception::Class(
'Example::Module::X::Args'
=> { alias => 'throw_args', },
'Example::Module::X::DB'
=> { alias => 'throw_db', },
);

Now, if your library needs to indicate that a database error has occured it can:

throw_db("Text of the error message");

and these errors will be distinguishable from errors due to incorrect arguments. More importantly, these errors are distinguished by the class in which they are defined, rather than the text of the error message — distinguishing errors programmatically by the text of error messages is fragile, and prone to problems when messages are changed, either because the text has been reworded, or it has been translated.

You can take this further and produce class hierarchies of exceptions as necessary.

use Exception::Class(
'Example::Module::X::Args'
=> { alias => 'throw_args', },
'Example::Module::X::DB',
'Example::Module::X::DB::Connect'
=> { alias => 'throw_db_connect', },
'Example::Module::X::DB::Prepare'
=> { alias => 'throw_db_prepare', },
'Example::Module::X::DB::Query'
=> { alias => 'throw_db_query', },
);

This allows for distinguation of exceptions caused by argument parsing, database connections, query preparation, and query execution.

No comments: