I like my code to "work" the way I expect it to, but I also like it to "not work" the way I expect it to as well.
When I test my modules, I test with correctly formed input and assure that it behaves as it is expected (and documented) to do. I also test with incorrectly formed input and assure that it behaves as expected (and documented) as well. I want to be sure that if I am expecting you to pass a hash-ref, that I will never accept an array-ref in its place. It is not enough to have perl complain the first time I try to do $arg->{"key"} on an array ref. I want to catch things prior to that, and not allow any part of my methods to be run unless it receives the proper input.
This is a very small part of an idea called Design By Contract. Design By Contract came from the language Eiffel, in which it is a compiler option which can enable and disable the checking of post and pre-conditons for methods as well as class invariants. There is a perl implementation of Design By Contract called Class::Contract, and one for just subroutines in Sub::Assert. Both require either a wrapping of or a controlled generation of subroutines and methods and surely have some serious overhead involved. Personally, I very much like the idea of Design By Contract, because it helps me to feel confident in that my code works as I expect it to, but I don't like the idea that I had to sacrifice performance for confidence. Which brings me to the point of my meditation, that I developed a style of coding of which allows me to do efficient pre and post-condition checking on my methods.
One of my first attempts at this was to implement this with assert_* subroutines. However, this creates the overhead of an extra subroutine call and I didn't like how that slowed things down. I then tried just doing your normal if/unless blocks, but I found that my conditions were easily mistaken for program logic, and I really wanted them to be more distinct since they really were not part of the algorithm itself. Statement modifiers were another option, but they too got easily confused with the program text, and the conditional always ended up at the end of the line, rather then beginning and I wanted the conditionals to be the main focus when reading the code. I finally came upon the use of raw boolean expressions and the using the || operator as a short-circuit. Now this is no revolutionary perl idiom, we have all seen and used it for things like opening files.
Its simplicity and elegance had always made this idiom a favorite of mine, so I thought, why not adapt it for pre-post-conditions. Here is an example:open(FILE "<file.dat") || die "cannot open file";
Surely our $coin must be defined and it must be equal to 25 (some liberties taken in the simplification here), if not, then we cannot continue with our subroutine since our input is bad. Personally I choose to throw an exception, which some might find harsh, but is can just as easily be re-coded in a different way:sub insertQuarter { my ($coin) = @_; (defined($coin) && $coin == 25) || die "You must insert 25 cents"; initializeGame(); startGame(); }
There are any number of combinations available here, and all of which enjoy what I see as two important benefits to this style; speed and readability.# just return a false value and allow the # calling code to deal with the false return (defined($coin) && $coin == 25) || return 0; # warn the user of the wrong input and return # false, leaving the calling routine dealing # with things (defined($coin) && $coin == 25) || (warn("You must insert 25 cents"), +return 0); # use a custom error handler which can record # call stack information, set error variable, # and return a specific value. (defined($coin) && $coin == 25) || return errorHandler("You must inser +t 25 cents");
Readability to me is obvious, our condition is simply a boolean expression which can be read from left to right with relative ease (for those who read left to right of course). The conditional is first in the statement, and is made more distinct by being a bare expression and starting with a paren and therefore separating it visually from the function's body code. Of course I realize this observation might be a little subjective, and many may not agree with me, however the second quality, speed, I can prove.
To help prove this, I created a benchmark script which tests the various ways in which pre-conditons can be checked.
#!/usr/bin/perl use strict; use warnings; use Benchmark qw(:all); sub IfBlocks { my ($test) = @_; if ($test < 50) { die "Test is wrong"; } return $test; } sub UnlessBlocks { my ($test) = @_; unless ($test > 50) { die "Test is wrong"; } return $test; } sub IfStatement { my ($test) = @_; die "Test is wrong" if ($test < 50); return $test; } sub UnlessStatement { my ($test) = @_; die "Test is wrong" unless ($test > 50); return $test; } sub Assert { my ($test) = @_; ($test < 50) || die "Test is wrong"; return $test; } sub Assert2 { my ($test) = @_; ($test > 50) && die "Test is wrong"; return $test; } my @nums = map { ((rand() * 100) % 50) } (0 .. 50); cmpthese(10000, { 'IfBlocks' => sub { eval { IfBlocks($_) } for (@nums +) }, 'UnlessBlocks' => sub { eval { UnlessBlocks($_) } for (@nums +) }, 'IfStatement' => sub { eval { IfStatement($_) } for (@nums +) }, 'UnlessStatement' => sub { eval { UnlessStatement($_) } for (@nums +) }, 'Assert' => sub { eval { Assert($_) } for (@nums +) }, 'Assert2' => sub { eval { Assert2($_) } for (@nums +) }, });
Benchmark: timing 10000 iterations of Assert, Assert2, IfBlocks, IfSta +tement, UnlessBlocks, UnlessStatement... Assert: 4 wallclock secs ( 2.38 usr + 0.02 sys = 2.40 CPU) + @ 4166.67/s (n=10000) Assert2: 4 wallclock secs ( 2.36 usr + 0.03 sys = 2.39 CPU) + @ 4184.10/s (n=10000) IfBlocks: 13 wallclock secs ( 7.82 usr + 0.13 sys = 7.95 CPU) + @ 1257.86/s (n=10000) IfStatement: 13 wallclock secs ( 7.69 usr + 0.01 sys = 7.70 CPU) + @ 1298.70/s (n=10000) UnlessBlocks: 12 wallclock secs (10.68 usr + 0.80 sys = 11.48 CPU) + @ 871.08/s (n=10000) UnlessStatement: 11 wallclock secs (11.67 usr + 0.05 sys = 11.72 CPU) + @ 853.24/s (n=10000) Rate UnlessStatement UnlessBlocks IfBlocks IfStateme +nt Assert Assert2 UnlessStatement 853/s -- -2% -32% -3 +4% -80% -80% UnlessBlocks 871/s 2% -- -31% -3 +3% -79% -79% IfBlocks 1258/s 47% 44% -- - +3% -70% -70% IfStatement 1299/s 52% 49% 3% +-- -69% -69% Assert 4167/s 388% 378% 231% 22 +1% -- -0% Assert2 4184/s 390% 380% 233% 22 +2% 0% --
I have been using this style for a few years now, and have found it not only to be very easy maintain, but it can serve as a form of self-documentation on my methods. The guidelines for acceptability of my arguments is directly codified, and properly written die or warn messages can go a long way. You can even use this to substitute for lack of type-checked arguments in perl like this:
sub onlyAcceptFooObjects { my ($foo) = @_; (defined($foo) && ref($foo) && $foo->isa("Foo")) || die "This method only accepts Foo objects"; # ... }
And of course, you don't have to confine this to just pre-conditons, anywhere you need to make an assertion before you can continue your code, this style works. I have many times used this to simplify a complex conditional by performing pre-flight checks on variables before entering the conditional code. This serves to reduce the need to include some edge cases in my conditional logic, therefore making it simpler.
This style may not be for all, but I know that for me, it has given me the benefits of some of the concepts found in things like Design By Contract, without the heavy overhead that sometimes comes with them.
UPDATE: minor code change/fix thanks to itub.
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: Being more Assert-ive with Perl
by itub (Priest) on Sep 17, 2004 at 15:07 UTC | |
by stvn (Monsignor) on Sep 17, 2004 at 15:12 UTC | |
Re: Being more Assert-ive with Perl
by ambrus (Abbot) on Sep 17, 2004 at 14:28 UTC | |
Re: Being more Assert-ive with Perl
by kappa (Chaplain) on Sep 17, 2004 at 14:59 UTC | |
by stvn (Monsignor) on Sep 17, 2004 at 15:09 UTC | |
by kappa (Chaplain) on Sep 17, 2004 at 15:33 UTC | |
by stvn (Monsignor) on Sep 17, 2004 at 15:42 UTC | |
by kappa (Chaplain) on Sep 17, 2004 at 16:18 UTC | |
Re: Being more Assert-ive with Perl
by DrHyde (Prior) on Sep 17, 2004 at 16:04 UTC | |
by stvn (Monsignor) on Sep 17, 2004 at 16:38 UTC | |
by antirice (Priest) on Sep 18, 2004 at 02:10 UTC | |
by ysth (Canon) on Sep 19, 2004 at 03:26 UTC | |
Re: Being more Assert-ive with Perl
by water (Deacon) on Sep 18, 2004 at 08:08 UTC | |
by stvn (Monsignor) on Sep 18, 2004 at 15:18 UTC | |
Re: Being more Assert-ive with Perl
by autarch (Hermit) on Sep 18, 2004 at 03:27 UTC | |
by stvn (Monsignor) on Sep 18, 2004 at 04:52 UTC | |
by autarch (Hermit) on Sep 19, 2004 at 16:05 UTC | |
by stvn (Monsignor) on Sep 19, 2004 at 20:08 UTC | |
by stvn (Monsignor) on Sep 18, 2004 at 15:31 UTC | |
Re: Being more Assert-ive with Perl
by Zed_Lopez (Chaplain) on Sep 18, 2004 at 00:28 UTC | |
by stvn (Monsignor) on Sep 18, 2004 at 15:58 UTC | |
Re: Being more Assert-ive with Perl
by muba (Priest) on Sep 23, 2004 at 15:51 UTC | |
by stvn (Monsignor) on Sep 23, 2004 at 16:00 UTC |