Issue transitioning to EXPORT_OK from EXPORT

117 Views Asked by At

According to Perl Critic we should "Export symbols via @EXPORT_OK or %EXPORT_TAGS instead of @EXPORT"

Following this advice I am transitioning my module to @EXPORT_OK from @EXPORT, but I am having difficulty with functionality around Exporter. Below I will give an illustrative example of the situation.

Original Code

Originally my code looked like this and it worked as expected:

lib/MyAnimals/Cats.pm

package MyAnimals::Cats;

use parent qw(Exporter);

our @EXPORT = qw(tabby calico abyssinian tiger panther leopard);

lib/MyAnimals/Dogs.pm

package MyAnimals::Dogs;

use parent qw(Exporter);

our @EXPORT = qw(dachshund poodle great_dane st_bernard);

lib/MyAnimals.pm

package MyAnimals;

$Exporter::Verbose = 0;

sub import {
    my $self   = shift;
    my $caller = caller;

    foreach my $package (@_) {
        my $full_package = "MyAnimals::$package";
        eval "require $full_package";
        if ($@) {
            warn "Could not require MyAnimals::$package: $@";
        }

        $full_package->Exporter::export($caller);
    }
    return;
}

=head2 How it works

The MyAnimals module simply imports functions from MyAnimals::* modules.
Each module defines a self-contained functions, and puts those function
names into @EXPORT. MyAnimals defines its own import function, but that
does not matter to the plug-in modules.

This function is taken from brian d foy's Test::Data module. Thanks brian!

This allowed me to have in my program:

program.pl

#!/usr/bin/env perl

use MyAnimals qw(Cats Dogs);

Which imported all of the cats and dogs (functions) into the program.

New Code

However, since it is considered bad form to pollute a user's namespace by exporting everything instead of just what they asked for, I am moving to this code:

lib/MyAnimals/Cats.pm

package MyAnimals::Cats;

use parent qw(Exporter);

our %EXPORT_TAGS = (house => [qw(tabby calico abyssinian)], jungle => [qw(tiger panther leopard)]);

Exporter::export_ok_tags('house');   # add tabby calico abyssinian to @EXPORT_OK
Exporter::export_ok_tags('jungle');  # add tiger panther leopard to @EXPORT_OK

# add all the other ":class" tags to the ":all" class, deleting duplicates
 {
   my %seen;

   push @{$EXPORT_TAGS{all}},
     grep {!$seen{$_}++} @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS;
 }

lib/MyAnimals/Dogs.pm

package MyAnimals::Dogs;

use parent qw(Exporter);

our @EXPORT = qw(dachshund poodle great_dane st_bernard);

our %EXPORT_TAGS = (small => [qw(dachshund poodle)], big => [qw(great_dane st_bernard)]);

Exporter::export_ok_tags('small');   # add dachshund poodle to @EXPORT_OK
Exporter::export_ok_tags('big');     # add great_dane st_bernard to @EXPORT_OK

# add all the other ":class" tags to the ":all" class, deleting duplicates
 {
   my %seen;

   push @{$EXPORT_TAGS{all}},
     grep {!$seen{$_}++} @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS;
 }

This now allows my program to import only the animals it wants:

program.pl

#!/usr/bin/env perl

use MyAnimals::Cats qw(:jungle tabby);
use MyAnimals::Dogs qw(:big);

Problem

However, I have lost the functionality of MyAnimals.pm, specifically I no longer have a simple, clean way of importing eveything in those cases where I really want to, say in a test for example:

t/01-no_longer_works.t

use MyAnimals qw(Cats Dogs);

Now I have to do this: t/02-works_but_is_not_elegant.t

use MyAnimals::Cats qw(:all);
use MyAnimals::Dogs qw(:all);

Question

What I am looking for is a way to change MyAnimals.pm so that the test t/01-no_longer_works.t functions as it previously did (when I was using @EXPORT) and imports all of the animals (functions).

2

There are 2 best solutions below

2
brian d foy On

First, don't inherit from Exporter (unless you are making a fancier and more specific exporter yourself). It's enough to get its import routine:

use Exporter qw(import);

Next, it looks like most of your problem is that you want to export from MyAnimals::Cats through MyAnimals to the final caller. That's something that I'd typically try to avoid. It's fine to play around with this sort of thing, but I'd hate to have to deal with that in production code.

Having said that, Exporter has another routine, export_to_level, that allows you to export further up the chain. With this you can effectively skip that MyAnimals and export to whatever called MyAnimals.

Here's MyAnimals, in which you have to define your own import and do a lot of work. Remember that you can't just ask every module to import everything because they only export their own stuff (although you can goof around by them having the same export tags I guess). I've left all that bookkeeping and fiddling out of this:

package MyAnimals;

sub import {
    shift;
    require MyAnimals::Cats;
    MyAnimals::Cats->import( @_ );
    }

1;

Now, MyAnimals::Cats, which is now a specialized importer so it inherits from Exporter. You can't get to export_to_level otherwise:

package MyAnimals::Cats;
use parent qw(Exporter);

You may also like to look at the source for Modern::Perl to see how it imports a bunch of things from different sources.

sub import {
    __PACKAGE__->export_to_level(2, @_);
    }

our @EXPORT_OK = qw(foo bar);

sub foo { print "foo from Cats\n"; }
sub bar { print "bar from Cats\n"; }

1;

And then the test program, which only loads MyAnimals and asks for two things:

use lib qw(.);
use MyAnimals qw(foo bar);

foo();
bar();

The output shows that I got what I wanted:

$ perl test.pl
foo from Cats
bar from Cats

But, this is not a good design if there's any other way you could get this to work. There are various things to recognize the modules that you want to load and then call their import routines with the :all tag like you have shown. However, you are back to where you started where you are exporting everything anyway. If you are going to do that, just more complicated, save yourself the trouble by doing nothing at all.

And remember, there is no such thing as a best practice with no context. Perl Best Practices was never meant to be hard and fast rules (read Damian's intro again). Do what makes sense for the problem at hand. There are many cases where I'd rather have default exports that I can turn off rather than things I have to enable every time:

use File::Basename;             # I probably want the usual suspects (common)
use File::Basename qw();        # I want nothing (more rare)
use File::Basename qw(dirname); # I want one thing only; this is the code either way

Part of Perl's philosophy was to make the things you do all the time easy (less typing), while still giving you a way to do the things you do rarely. This gets lost for the people who want to make all code look alike no matter the intent of the code.

17
ikegami On

Identifying the problem

I am transitioning my module to @EXPORT_OK from @EXPORT

Using @EXPORT_OK forces a module's user to explicitly list what it imports from a module. This allows someone reading, debugging or changing the code to quickly see from where an imported symbol originates.

With your module, the caller would use the following to import a symbol from MyAnimals::Cat or MyAnimals::Dog.

use MyAnimals qw( X );

But that's already means "load module MyAnimals::X". It's incompatible to have it "import symbol X" as well.

Fundamentally, the problem you are facing is an interface design failure.


Example design solution

What if a leading :: denotes a package to load, and an absence denotes a symbol (or tag) to import? With that interface, one could use the following:

use MyAnimals qw( ::Cat );
use MyAnimals qw( ::Dog );
use MyAnimals qw( meow );

or

use MyAnimals qw( ::Cat ::Dog meow );

Implementation of example design solution

package MyAnimals;

use Exporter   qw( );
use List::Util qw( uniq );

our @EXPORT_OK = ();
our %EXPORT_TAGS = ( ALL => \@EXPORT_OK );  # Optional.

sub import {
   my $class = shift;

   my ( @pkgs, @rest );
   for ( @_ ) {
      if ( /^::/ ) {
         push @pkgs, __PACKAGE__ . $_;
      } else {
         push @rest, $_;
      }
   }

   for my $pkg ( @pkgs ) {
      my $mod = ( $pkg =~ s{::}{/}gr ) . ".pm";
      require $mod;

      my $exports = do { no strict "refs"; \@{ $pkg . "::EXPORT_OK" } };
      $pkg->import( @$exports );
      @EXPORT_OK = uniq @EXPORT_OK, @$exports;
   }

   @_ = ( $class, @rest );
   goto &Exporter::import;
}

1;

So if the "sub modules" look like this

package MyAnimals::Cat;

use Exporter qw( import );

our @EXPORT_OK = qw( meow );
our %EXPORT_TAGS = ( ALL => \@EXPORT_OK );  # Optional.

sub meow { print "meow\n"; }

1;
package MyAnimals::Dog;

use Exporter qw( import );

our @EXPORT_OK = qw( woof );
our %EXPORT_TAGS = ( ALL => \@EXPORT_OK );  # Optional.

sub woof { print "woof\n"; }

1;

you get output like this

$ perl -I lib -e'
   use MyAnimals qw( ::Cat ::Dog meow woof );
   meow;
   woof;
'
meow
woof