SPUG: Overly clever?

Andrew Sweger andrew at sweger.net
Fri May 21 10:54:09 CDT 2004


Some code that I have adopted finally got to the point of requiring more
disciplined practices. First, a little background on the problem. The
original code was of the style I call pseudo-modular. For example, the
"module" looked like this:

File: Whizbang.pm
-----------------
package Whizbang;
sub foo {...}
sub bar {...}
1;
__END__


The script that depends on Whizbang looks like this:

File: whiz.pl
------------------
#!/usr/bin/perl

use strict;
use Whizbang;
package Whizbang;          # <----- Seriously!

sub baz {&foo();&bar()}
__END__


I worked with it like this for a while, but it really got on my nerves
basically never knowing where functions were defined. Or worse, not having
any compile-time checks with 5,000 lines of code that has no tests. Even
now, I think I've run at most 20% of the code. I'm not sure how some code
paths are reached (there are other evils lurking in there).

So, I did some basic things that are automatic for me. I added lexical
warnings, made the module a little more real by having it export functions
into the callers package, stopped using global package variables
everywhere, and put the main script back into package main. I know that
it's not fashionable to export symbols into the caller's namespace by
brute force and I want to be aware of which functions are actually being
used (so I can further encapsulate the module at a later point). This next
bit is the part I'm worried about.

First, an explanation. My goal is up-front accounting of functions and
when they're used (because there are more than I can keep track of)
without radically changing how all the code works or is used (it's a
"Martha Stewart" remodel, not a "This Old House" remodel; i.e.,
superficial versus tearing it down to the studs). I renamed all the
functions in the module to the form _function_name to indicate they are
internal to the module and not meant to be exported. Then I made a list of
all the functions in the module and assigned it to the constant FUNCTIONS.
If a function is meant to be exported, its name is listed without the
leading underscore (in addition to the one with the underscore). This
gives me a catalog of all the functions in the module. I can make a
function exportable by listing it sans leading underscore in its name.
Then several other steps, that are executed when the module is first
use'd, check to make sure the catalog of functions actually exist and
creates aliases for the exportable functions. Note, I don't actually use
the :all export tag unless I'm terribly desparate. I prefer to explicitly
name which functions must be imported from Whizbang at use time.

This is my (vastly simplified) implementation of exporting functions in
the module:

File: Whizbang.pm
-----------------
package Whizbang;
use warnings;
use strict;
use Exporter 'import';
use vars qw(%EXPORT_TAGS);
use contant FUNCTIONS => qw(
	 sub_one
	_sub_one
	_sub_two
	 sub_three
	_sub_three
);      # There are hundreds of functions in the real thing
use subs FUNCTIONS;

my @EXPORTABLE = grep !/^_/, FUNCTIONS;
%EXPORT_TAGS = ( all => [@EXPORTABLE] );
Exporter::export_ok_tags('all');

foreach (FUNCTIONS) {
	next unless /^_/;
	warn "Catalog of functions contains non-existent sub $_"
	  unless defined &{Whizbang::{$_}};
}

foreach (@EXPORTABLE) {
	warn "Unable to export $_ because _$_ is not defined", next
	  unless defined &{Whizbang::{"_$_"}};
	eval "*$_ = \\&_$_";  # alias _function to function
}

sub _sub_one {...}
sub _sub_two {...}
sub _sub_three {...}
1;
__END__


The script would then be more like:

File: whiz.pl
------------------
#!/usr/bin/perl
use warnings;
use strict;
use Whizbang qw(
	sub_one
	sub_three
);

...
__END__


Now, the question: Have I created a monster? Is this trying to be overly
clever? Have I completely missed a simpler way to achieve my current goal?
It does seem to do the job of letting me know when I'm trying to call a
function that hasn't been both declared as exportable (via the method
above) *and* listed in the import array in the calling script. So now I
have a fairly good accounting of functions without a any changes to the
old code.

A related question (or maybe it's just a gripe), why can't Perl complain
at compile time about undeclared subroutines when they called as
function() or &function instead of just bareword usage? At least as an
option, I'd like to test the code for the problem of calls to functions
that haven't been imported at compile-time (rather than hit or miss
complaints at run-time about undefined subroutines).

In other news, line folding (as it's called in Vim) is very cool. Ask for
a demo when you see me!

-- 
Andrew B. Sweger -- The great thing about multitasking is that several
                                things can go wrong at once.






More information about the spug-list mailing list