Autodie 2.00 released
This weekend the long awaited autodie 2.00 for
Perl was released to the CPAN, which was almost immediately replaced
by 2.02, which fixes some oopsed tests and which adds a couple more features to
give us a really sweet experience. This blog entry assumes you're
using 2.02.
Observant viewers will notice that the major version number has
changed. I've taken the great leap from 1.999 to 2.00. Clearly,
something is different, and you might be wondering what.
Well, autodie 2.0 now supports a hinting
interface for user-defined subroutines. Put simply, if you have a
user-defined subroutine that does something funny to signify failure,
you can now tell autodie about that. Once it knows, it can Do The
Right Thing when checking your subroutine. You can even put the hints
into the same file as those subs, and if someone is using autodie
2.00, it will find the hints and use them.
This may not sound very exciting, but it is. It means that a lot of
really ugly error-checking code, both on the CPAN and the DarkPAN, can
go away. Lexically. Still not convinced this will change your life?
Let's look a little more closely; trust me, you'll like it.
Let's pretend you're working on a piece of legacy code. For some
reason, the people who wrote this code decided the best way to
signal errors is by returning the list (undef, "Error
message"). I don't know why, but I've seen this anti-pattern
emerge independently in three 100k+ line projects I've been
involved in.
sub some_sub {
if ( not batteries_full() ) {
return ( undef, "insufficient energy" );
}
if ( not coin_inserted() ) {
return ( undef, "insufficient credit" );
}
my @results = some_calculation();
return @results;
}
If you want to check to see if some_sub() returns an error,
you need to capture its return values, look at the first one to see if
it's undefined, and if it's not, use the second one as your error.
At least, that's what you're supposed to do.
What actually happens is most developers decide that's way too hard,
and don't bother checking for errors. Then one day, the batteries on
your doomsday-asteroid-destroying-satellite go flat, nobody notices,
and through an ironic twist of fate you're left as the last known human
survivor, and there are zombie hordes and walking killer plants
outside.
So, how can autodie help us?
Well, before version 2.00, it couldn't. But now, with
autodie::hints,
it can! We can give autodie hints about how the return values are
checked. They look like this:
use autodie::hints;
autodie::hints->set_hints_for(
'Some::Package::some_sub' => {
scalar => sub { 1 },
list => sub { @_ == 2 and not defined $_[0] },
},
);
Our hints here are simple subroutines. If they return true, our
subroutine has failed. If they return false, it's executed
successfully. Notice that our scalar hint always returns true.
That's because we consider any call of our subroutine in scalar
context to be a mistake. It's returning a list of values, and
you should be checking that list.
Once we've set our hints, we can then use autodie to automatically
check if we're successful:
use Some::Module qw(some_sub);
sub target_asteroid {
use autodie qw( ! some_sub );
# autodie has lexical scope, so only calls to some_sub inside
# the target_asteroid subroutine are affected.
my @results = some_sub(); # Succeeds or dies
}
sub target_ufo {
my @results = some_sub();
# autodie is out of lexical scope, so we have to manually
# process @results here.
}
If you're wondering what that exclamation mark means, it means "insist
on hints", and is a new piece of syntax with autodie 2.00. If for any
reason autodie can't find the hints for some_sub, our code
won't compile. That's a very good thing, and avoids us having a false
sense of security if we use autodie on an unhinted sub.
However the error messages from autodie aren't really that useful.
They're going to be things like "Can't some_sub() at
space_defense.pl line 53". There's a noticable lack of
explanation as to why some_sub() failed.
Luckily, since the way early versions of autodie, we've been able
to register message handlers. And with the new features in autodie
2.02, we can produce very rich messages. Let's see how!
use autodie::exception;
autodie::exception->register(
'Some::Module::some_sub' => sub {
my ($error) = @_;
if ($error->context eq "scalar") {
return "some_sub() can't be called in a scalar context";
}
# $error->return gives a list of everything our failed sub
# returned. We know this particular sub puts the error
# message the second argument (index 1).
my $error_msg = $error->return->[1];
return "some_sub() failed: $error_msg";
}
);
Now, whenever some_sub() fails, it'll print a genuinely
useful message, like "some_sub() failed: Insufficient energy at
space_defense.pl line 53". Yes, autodie automatically adds the
file and line number for you. Nice!
But wait, there's more! We don't want to see this sort of code
floating around in your programs. You may be dealing
with other people's modules that you can't modify, so we can't hide
all this configuration in there. So, we can write our own
pragma that contains all this info. Here's the full
code for a theoretical my::autodie pragma, and is the exact
same code used by the t/blog_hints.t file in autodie's
test suite.
package my::autodie;
use strict;
use warnings;
use base qw(autodie);
use autodie::exception;
use autodie::hints;
autodie::hints->set_hints_for(
'Some::Module::some_sub' => {
scalar => sub { 1 },
list => sub { @_ == 2 and not defined $_[0] }
},
);
autodie::exception->register(
'Some::Module::some_sub' => sub {
my ($E) = @_;
if ($E->context eq "scalar") {
return "some_sub() can't be called in scalar context";
}
my $error = $E->return->[1];
return "some_sub() failed: $error";
}
);
1;
It works exactly the same as regular autodie, except it also knows
how to handle some_sub(), and display good looking error
messages. Here's how we'd use it:
use Some::Module qw(some_sub);
use my::autodie qw( ! some_sub );
my @results = some_sub(); # Succeeds or dies with a useful error!
There's a lot more you can do with autodie, and if you want to learn
more, I'd suggest coming to my talk at OSCON or YAPC::EU, where I'll be covering all this and more, with a distinctive Star Trek twist. ;)