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. ;)