Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Require Perl Modules using an Aliased Name

Tags:

perl

***The following is background to help explain what I've tried so far. If you'd prefer to read the main question first, skip to the bottom.***


Starting Out

My Baz module invokes a number of other modules, all similar, each of which is one level down in the namespace. The ones of interest here compose the Thing role. In addition to the individual require statements, the constant list ALL_THINGS enumerates the relevant Thing modules for use later on. My original code looks like so:

package Foo::Bar::Baz;

use constant ALL_THINGS => qw{ Foo::Bar::Baz::ThingA Foo::Bar::Baz::ThingB ... };

require Foo::Bar::Baz::ThingA;
require Foo::Bar::Baz::ThingB;
[...]

Eliminating Redundancy

As I mentioned, there are quite a lot of Thing modules, and I'm still adding more. Each time I create a new Thing class, I have to add a new require statement and also add the same identical text to the ALL_THINGS list. In order to avoid this duplication, I wanted to replace the individual require lines with a loop iterating over ALL_THINGS. I added this, which works fine by itself:

foreach my $module (ALL_THINGS) {
    eval "require $module";
}

However this solution doesn't seem to play well with my next change.


Improving Readability

The full module name for each Thing is long and unwieldy. I'd like to alias the package name to make it easier to type/read. I looked at Package::Alias, but it seems that will use them, which I'd like to avoid if possible. The best solution I've come to so far is the pattern suggested in this question:

BEGIN { *Things:: = *Foo::Bar::Baz:: ; }

This also works, in the sense that it allows me to use Thing::ThingA->classMethod. However, unsurprisingly, it doesn't work in the require loop above, as require Thing::ThingA searches @INC for Thing/ThingA.pm rather than Foo/Bar/Baz/ThingA.pm.


Main Question: Putting Them Together

I'd like to cut down the long package names (i.e. Foo::Bar::Baz::ThingA) in my ALL_THINGS list to Things::ThingA, but still be able to use that same list to build my require statements in a loop.

  • Is there a different way to alias Foo::Bar::Baz:: as Things:: such that I can require Things::ThingA?
  • Or, if I'm doing the alias part right, is there a way to dereference Things::ThingA to Foo::Bar::Baz::ThingA in (or before?) the eval so that require finds the correct package?
  • Is there some other generally accepted method of tying together packages at different levels of the same namespace to obviate the need for all this?

Bonus Questions (related to eval "require $x"):

  • In the perldoc for constant it says that constant lists are not actually read-only. Does that create a security concern with the use of eval?
  • If so, is there a safer way to do it without the need to load additional modules?
  • Being somewhat new to Perl, are there any more subtle differences I might have missed between this approach and my previous one (individual require statements for each module)?

Note: I accepted Dave Sherohman's answer, as it most fully addresses the question I asked. However, I ultimately implemented a solution based on lordadmira's answer.

like image 346
BryKKan Avatar asked Apr 25 '19 01:04

BryKKan


People also ask

What is a package alias?

An “alias” package is a symbolic name (reference) for another package (target).

What is a perl namespace?

In perl namespaces are called "packages" and the package declaration tells the compiler which namespace to prefix to our variables and unqualified dynamic names.

How do perl modules work?

The perl1 compiler (yes, there is a compiler although it's interpreted language) loads files, compiles them, then switches to run time to run the compiled code. When it encounters a new file to load, it switches back to compile time, compiles the new code, and so on. To load a module at compile time, you use it.


1 Answers

How black do you like your magic?

We all know that, in order to require modules, Perl looks through @INC to find the file it wants to load. One of the little-known (and even-less-used) aspects of this process is that @INC isn't limited to only contain filesystem paths. You can also put coderefs there, allowing you to hijack the module loading process and bend it to your will.

For the use case you've described, something like the following (untested) should do the trick:

BEGIN { unshift @INC, \&require_things }

sub require_things {
  my (undef, $filename) = @_;

  # Don't go into an infinite loop when you hit a non-Thing:: module!
  return unless $filename =~ /^Thing::/;

  $filename =~ s/^Thing::/Foo::Bar::Baz::/;
  require $filename;  
}

Basically what this does is, as the first entry in @INC, it looks at the name of the requested module and, if it starts with Thing::, it loads the corresponding Foo::Bar::Baz:: module instead. Simple and effective, but really easy to confuse future maintenance programmers (including yourself!) with, so use with caution.


As an alternate approach, you also have the option of specifying a package name in the module which doesn't correspond to the physical path of the file - the two are normally the same by convention, to make life easier when reading and maintaining the code, but there's no technical requirement for them to match. If the file ./lib/Foo/Bar/Baz/Xyzzy.pm contains

package Thing::Xyzzy;

sub frob { ... };

then you would use it by doing

require Foo::Bar::Baz::Xyzzy;
Thing::Xyzzy::frob();

and Perl will be perfectly happy with that (even though your coworkers may not be).


Finally, if you want to get rid of ALL_THINGS, take a look at Module::Pluggable. You give it a namespace, then it finds all available modules in that namespace and gives you a list of them. It can also be set to require each module as it is found:

use Module::Pluggable require => 1, search_path => ['Foo::Bar::Baz'];
my @plugins = plugins;

@plugins now contains a list of all Foo::Bar::Baz::* modules, and those modules have already been loaded with require. Or you can just call plugins without assigning the result to a variable if you only care about loading the modules and don't need a list of them.

like image 172
Dave Sherohman Avatar answered Sep 17 '22 20:09

Dave Sherohman