Long Import Lists in Perl (and available strategies for managing them)
I ran across this the other day while writing some sample code for the next chapter of Testing Strategies for Modern Perl.
How can we use long lists of symbols from an imported package and still keep the code readable?
I usually prefer use statements of the form:
use My::Module qw(symbol1 symbol2 symbol3);
Except for specially understood modules, like Moose
and Test::More
, I don’t like to just import everything. Rather I like to explicitly call out only the specific symbols I need.
But what if you need to:
use My::Module qw(
symbol1 symbol2 symbol3 symbol4 symbol5 etc and so many symbols
that it takes up several lines all the time in every package
that uses it
);
There are a few alternative approaches.
Just import everything
We could just fall back on importing everything.
So… Five years from now, I’m going to revisit that code, and I’ll see:
wiggle_foo_gadget(WIGGLE_WOBBLE);
And my first question is going to be, “Which of those 17 modules does that come from?!” And then I’ll need to grep through the entire codebase to find it.
That’s why I like to call out the specific symbols being imported, and that’s our standard practice here at The Perl Shop. If the symbol is explicitly imported, a simple search through the current module will find it, and it will be clear where the symbol came from.
Additionally, when we import everything, we have zero control over what symbols are imported. If a later version of a used module exports more symbols, then they’ll automatically get imported in our package, whether we want them there or not. This could result in name clashes and action-at-a-distance bugs down the line.
So alternative option #2…
Don’t import anything
That is, we can just use the full package name. So:
use My::Module ();do_something_with($My::Module::scalar_var);
do_something_else_with(@My::Module::array_var)
My::Module::do_the_hustle();
This is generally my preferred alternative. Additionally, reading My::Module::do_the_hustle()
, having the module cited adds context and can make the code easier to read.
But what if My::Module
is actually more like My::External::Module::With::Several::Levels
? Copying and pasting that monstrosity everywhere, that’s going to get pretty confusing pretty fast.
And in my sample code, this was indeed what I was facing. So not a viable alternative.
Use import tags
This is a well-established idiom, implemented directly by Exporter
.
With a tag, the use
line would look something like this:
use My::Really::Long::Module::Name qw(:some_symbols);
This is much more concise, and it gives us at least some level of control over what is being imported. But it still doesn’t make clear where the symbols came from.
This is the option I chose in my sample code. Mostly I did so for the reasons above. In production code, I would probably have opted for the very last of the options below (“Lexical aliasing”). But in this case, it was sample code for a testing book, and I really didn’t want to take a section of the book to explain that non-standard idiom, because it had nothing to do with testing.
Export a hash of symbols
This is very nonstandard, but one thought that occurs to me is that one could export read-only hashes of symbols.
So in My/Really/Long/Module/Name.pm:
package My::Really::Long::Module::Name;use Readonly;Readonly::Hash our %some_symbols => (
symbol1 => \&symbol1,
symbol2 => \&symbol2,
symbol3 => \&symbol3,
# ... and so forth
);
Then we can:
use My::Really::Long::Module::Name qw(%some_symbols);$some_symbols{symbol1}();
$some_symbols{symbol2}();
$some_symbols{symbol3}();
That’s a little awkward, but it is succinct (at least in the using module) and does identify the symbol source.
But it doesn’t exactly work with variables. That is, as soon as you put a reference to anything in a read-only data structure, the anything itself becomes read-only.
There’s a better alternative…
Alias the used package
That is, use My::Really::Long::Module::FooBar
and then refer to it as simply FooBar
.
We can accomplish this with Package::Alias
:
use Package::Alias FooBar => 'My::Really::Long::Module::FooBar';FooBar::make_it_rain();
say $FooBar::is_raining;
The Package::Alias
line above is actually syntactic sugar for:
BEGIN {
use My::Really::Long::Module::FooBar;
*{FooBar::} = \*{My::Really::Long::Module::FooBar::};
}
The disadvantage here is that the alias is global, not lexical. That is, if one module aliases FooBar
, then another module can’t also alias FooBar
(whether or not they’re aliasing to the same target). Package::Alias
will warn if you attempt that and then ignore the second and subsequent attempts.
(Note also that Package::Alias
will load the target package into the caller’s namespace only if it hasn’t been used previously. And it actually does use the default use
, which imports all available symbols into the caller’s namespace—but only if no other package has formerly loaded it. For this reason, this functionality is only appropriate for packages that do not export symbols. Otherwise, you need to explicitly use My::Module ()
before using Package::Alias
.)
Lexical aliasing
What I’d really like is to alias the target in lexical scope.
namespace::alias
claimed to do that. It uses Perl internals to accomplish its black magic. Unfortunately, there are several critical documented issues, and it has not successfully built since Perl 5.18. It was last released in 2012. So not an option.
aliased
does something similar. It creates a short-named subroutine that returns the long name of a target package. It also has magic to figure out what the subroutine should be named and other features. I haven’t tried the module, but it looks like quite an elegant design: simple and powerful. Unfortunately, it really only works for object-oriented code. So a tool for the toolbox, but not applicable to this blog post.
The best I was able to come up with was this:
use My::Really::Long::Module::FooBar ();my $FooBar = \%My::Really::Long::Module::FooBar::;$FooBar->{do_something}();
$FooBar->{do_something_else}();
my $is_done = $FooBar->{is_it_done}();$FooBar->{scalar_var}->$* = 'foo';push $FooBar->{array_var}->@*, qw(bar baz qux quux);$FooBar->{hash_var}{spam} = 'eggs';
This is slightly awkward, but it works. And it’s a nonstandard idiom, but it’s not so confusing that a competent Perl programmer can’t quickly figure out what’s going on. And maybe if we used it more, it would catch on.
This is an idiom I will keep in mind for future projects.