The problem with Exporters (Meet Importer)

The problem with Exporters

With Exporter, and most exporter tools we have failed to separate concerns. Exporting fundamentally involves 2 parties: exporter and importer. Historically however we have only ever concerned ourselves with the exporter. The "standard" way of dealing with this in perl has been to have a module that provides exports use it's import() method to inject symbols into the importer's namespace.

What if we did this with other similar concepts? What if instead of:

use base 'foo';

we had:

package My::Base;

# Gives us an import() method that sets @{caller::ISA} = __PACKAGE__
use base;

...

package My::Subclass;
use Base::Class; #Automatically sets @ISA

Most of you are probably cringing right now. I know in the past I have actually used this pattern and gotten slapped on the wrist for it (rightly so). But this is essentially what we are doing with our current exporters.

This has lead to many tools to try and work around the madness. We have several Exporter alternatives, some provide an alternate way of specifying exports, some provide better features for the importer, and some do both. What if you like Exporter::Declare's interface for creating an exporter, but much prefer Sub::Exporter's interface on the importing side? Tough luck, pick one. We don't have this problem when picking between 'parent' and 'base' for subclassing.

We CAN fix this problem. We need to separate our concerns.

Part 1: Meet Importer

I have uploaded the Importer module on cpan. This module acts on behalf of the consumer of exports. Instead of

use Carp qw/croak/;

do

use Importer 'Carp' => qw/croak/;

What this does is turn Exporter into a specification for declaring that a module has exports. Subclassing Exporter, or importing its import() method are no longer required though. Importer is 100% compatible with all current Exporter features and variables. Because of this you can now have a module that specifies exports, but does not have an import() method, or has an import() method that does something else completely.

Importer has 0 non-core dependencies. Even core dependencies are used sparingly. The only use/require statements it contains are for strict, warnings or to load the module that has the exports (on demand).

The Importer module also has a hook that should allow most other exporter modules on cpan to make themselves compatible with it. I will update Exporter::Declare in the near future. Other exporters will NOT have to use the package variables Exporter expects to work with Importer. You can even directly use the hook in your module to specify your exports (it is just a sub you define in the exporting package)

Part 2: Blead Patch?

Note: This part is not essential. I will be perfectly content just having Importer on cpan.

I have a perl-blead patch in the works that puts Importer into blead, further it updates Exporter in the following ways:

  • The code is a lot cleaner, and a lot smaller
  • There is no longer a complex and confusing caching layer
  • Exporter::Heavy is unnecessary (and mostly empty)
  • Preserves optimal path for simple 1-to-1 sub importing feature
  • Complex importing, or special features are handled via Importer (loaded on demand).
  • Unexpectedly improves performance!

This patch currently causes 1 test failure, and based on a conversation with #p5p it is not likely a real problem, just some large strings that need to be updated (fragile test). That said nobody has touched the test in years and we are not completely sure yet.

I have also tested this change against my canary list of cpan modules, and the current iteration works fine for all of them.

Having Exporter defer to Importer for anything more complex than a 1-to-1 sub import has the benefit that the modules will not grow out of sync. This is helpful, but not required, for maintaining compatibility. It also makes it a lot less likely anyone will ever have to touch Exporter again.

I would also be perfectly happy combining Exporter and Importer into a single dist, and I have no strong opinions for if it would be p5p upstream, or cpan upstream. This depends entirely on what p5p wants to do, they could also just outright reject the proposal.

Motivation

While working on my Test::Stream/Test2 work I found myself needing the ability to rename subs I imported. This was frustrating as Exporter does not support this feature. In addition the exporters that do are non-core, and not likely to ever be core (Test::Builder can't use non-core modules). I initially solved this by writing my own exporter as part of Test::Stream, but this generated significant negative feedback. Ultimately I was able to get around the problem completely, but I still wanted to solve it as a separate project.

A while back I submitted a proposal and patch to p5p for an enhancement to Exporter. All in all the reception to the idea of the new features was good. There were a lot of opinions on how it should be done, or what side effects there may be, but nobody seemed to outright reject the idea of the features. The things that were brought up really did stick we me though, in particular:

  • Burden of dependency

    To use the new features the consumer is obligated to list the newer Exporter version as a dep, this seems backwards.

  • import() modifying consumers namespace

    Having an exporter work by modifying the consumers namespace seems backwards, we don't have base classes modify a consumer's @INC on import, we have parent and base.

The thread fizzled out partly due to these concerns, partly because the patch did not go far enough in adding the desired features (some people on p5p wanted it to do more), and also a lack of time/energy on my part due to Test2/Test-Stream work. Lately I had some spare time, and I decided to come back to this problem from this new angle.

Leave a comment

About Chad 'Exodist' Granum

user-pic I write solutions to make things easier.