Writing Robust Perl Code
Erwan Lemonnier
Swedish Premium Pension Authority (PPM)
$Revision: 1.12 $
Robust Code?
- Low bug rate
- Well tested
- Resistant to structural change (refactoring, evolution)
- Resistant to outside change (API, data bugs)
- Resistant to dev team change
Robust Perl Code? 1/3
- "robust code with a scripting language? you must be joking!!"
- 'scripting language'...
- Many corporate programmers do not understand dynamic languages
- Management barely knows what it is
Robust Perl Code? 2/3
- Perl has the wrong reputation
- Perl has let generations of bad programmers write working code
- Perl code can (does?) look awfull
- I mean, how serious is this:
s''(q.S:$/9=(T1';s;(..)(..);$..=
substr+crypt($1,$2),2,3;eg;print$..$/
Robust Perl Code? 3/3
- More serious issues:
- Perl syntax confuses non-Perl programmers
- Perl lacks strong typing
- Perl relies on context and assumes too much.
- Example:
$b = '0'; # string context, but numeric intention
$a = - $b; # numeric context
print "$a"; # gives '-0'...
Killing myths
- You can write business critical code in Perl
- It's hard, as with any language
- But not harder
- (...and possibly much easier)
So... bug-proof your Perl code!
Issue number 1:
- Bad Perl programmers are dangerous to Perl code
- Bad Perl code is dangerous to good programmers
- So protect your code from programmers!
Education
- Educate your dev team:
- Code Complete
- O'reilly Perl Bookshelf
- Perl Best Practice
- Perl Testing
- Go to conferences
- Read the source of good CPAN modules
- Seek knowledge
Keep Code Simple
- KISS (Keep It Simple Stupid)
- Bad programmers will bug down your code if they don't understand it
- No fancy Perl coding!
- No smart programming!
- Comments where needed!
Use simple syntax
- foreach instead of map or grep
- while instead of do-until
- if+negation instead of unless
- if instead (test)? true : false
- no gotos
- and so on...
Comment regular expressions
the \x modifier:
$a =~ /^\d # start one digit
_ # followed by an underscore
[a-z]{10} # and ten small case letters
$ # and nothing more
/x;
When you can't avoid magic
- Sometimes you just have to
- Alter a class hierarchy during runtime
- Generate code dynamically
- Use syntax magic
- This kind of code is very fragile
- So protect it from *bad* programmers!
Protect advanced Perl code
- Assert all input and state data
- Isolate advanced code in modules
- Surround it with clear warning signs
- to scare away bad programmers
- Document the API carefully
- Write VERY THOROUGH test suites
- Focus on test coverage (Devel::Cover)
Code Standard
- Have a code standard
- Which one does not really matter
- Enforce your code standard
- Perl::Tidy
- Perl::Critic or Module::Checkstyle
- and (CVS|SVN|*) filters
Code Review
- Code must be read by others
- Code must be criticized by others
- Code must be tested by others
Open Source your Code
- Requires quality
- More will test
- More will criticize
- Code gets better
- Put the source on sourceforge and release it on CPAN!
Personality
- Be honest with yourself!
- Don't write anything you don't understand
- This is especially important with Perl ;)
- "the bug is in me"
- Expect to make mistakes
- If your code is bugged, it's probably your fault
- Share your problems: talk with others
Issue number 2:
- Change in business logic are dangerous to code
- So make sure you notice it if they break something
Assert the impossible
example:
# by design, $a can only be 'foo' or 'bar'
my $a = get_a();
# but...
if (!defined $a || ref $a ne '') {
die "impossible!";
} elsif ($a eq 'foo') {
do_foo;
} elsif ($a eq 'bar') {
do_bar;
} else {
die "impossible!";
}
Test, TeSt, tESt (TEST!)
- Probably the most important robustising technic
- Use test coverage (Devel::Cover)
- Design for testing
- Break your code into self-standing private functions ('_name') with clear internal APIs
- Test each of those private functions
- Automate the test run
- Store test results in a database with a nice UI (smolder?)
Test, TeSt, tESt (TEST!)
- Make tests mandatory
- Have a test that tests that modules have tests!
- Improve coverage by simulating the impossible:
BEGIN {
*CORE::GLOBAL::open = sub {
return 0;
};
}
my $in = open('file') || die "error: $!\n";
Issue number 3:
- Change in input data format is dangerous to the code
Be Paranoid
- Defensive programming:
- trust no one
- trust no input data
- trust not even yourself
- Assert everything!
- Assert the impossible!
Database
- Don't spread DBI code all over your source
- Use a database abstraction layer
- Class::DBI
- DBIx::Class
- Jifty::DBI
- Catalyst
- But beware: too much abstraction may hide some flaws in your design
- You may want explicit SQL (for reasons of efficiency or clarity)
Issue number 4:
- However hard you may try, you will still have bugs
- So be prepared to face them
Logging
- Log everything!
- What your program does
- Programs/files versions
- Match any data alteration to its cause
- Log::Log4perl
- Log::Dispatch
Debug logs
- Keep even debug logging
- Just turn it off in production
- To turn on logging selectively during runtime, try Hook::Filter:
use Log::Dispatch;
use Hook::Filter rules => 'filter_rules.conf',
hook => ['Log::Dispatch::log',
'Log::Dispatch::log_to'];
Exceptions
- Crash, don't trash
- Croak, don't die
- Write usefull error messages
Ok ok...
...bored?
Free the dogs! programming by contract!
Background
- Perl is weakly typed
- A variable may contain any type of data at any time
- Many consider weakly typed languages unapropriate to robust programming
- This is usually compensated by writing assert code in the beginning of each subroutine, checking the type and state of arguments
Contract programming 1/2
- What is it?
- Defines
- pre/post conditions
- invariants
- on function/method/accessor calls
- Conditions are compiled at runtime and wrapped around subs
Contract programming 2/2
- The good:
- Emulate dynamic type checking, to compense for Perl's weak typing
- Better: assert input arguments and results
- The bad:
- The programming by contract paradigm is badly defined
- Heavy syntax
- Unclear error diagnostics
- Inheritance issues
Class::Contract 1/2
- Closest to the original Eiffel definition
- Add design-by-contract, ie the class is defined in the contract
- Example:
Class::Contract 2/2
package ClassName
use Class::Contract;
contract {
attr 'data1' => HASH;
method 'methodname';
pre { ... };
failmsg 'Error message';
post { ... };
failmsg 'Error message';
impl { ... };
};
Class::Agreement
- Same thing, different syntax
use Class::Agreement;
precondition foo => sub {
my ( $self, $value ) = @_;
return ( $value >= 0 );
};
sub foo {
my ( $self, $value ) = @_;
...
}
Issues
- Class::Contract: syntax is too requiring for general use
- Class::Agreement: less constraint, but still heavy
- Pragmatic use of contracts:
- validate type of input/return arguments
- and replace some assert code
We want something like
use Sub::Contract;
use Check::MyTypes;
contract('foo')
->in(\&is_foo_object, \&is_integer)
->out(\&is_string);
sub foo {
my ($self, $int) = @_;
return "value is $int";
}
More universal
- Much easier to have generic type/state checking code...
- ...rather than generic pre/post checking code
- Put all type/state checking code in external modules
Contract pool?
- What about having dynamic contracts?
- And what about having contract objects?
- And storing them in a pool?
- So you can do things like:
use Sub::Contract::Pool;
my $pool = get_contract_pool(); # singleton
my @contracts = $pool->find(name_matches => "^My::Module::do_*$");
map { $_->disable } @contracts;
Caching?
use Sub::Contract;
contract('foo')
->in(\&is_foo_object, \&is_integer)
->out(\&is_string);
->memoize(size => 1000);
sub foo {
my ($self, $int) = @_;
return "value is $int";
}
Sub::Contract
- Sub::Contract exists
- Now on sourceforge and CPAN
Contract programming...
- Sub::Contract is used in production
- Pragmatic contracts works well for replacing assertion/type checking
- Easily added to existing code
- Perl6 will have constraints and roles instead
Questions?
Thank you!