Perl Best Practices

17.6. Declarative Exporting

Consider exporting declaratively.

The Exporter module has served Perl well over many years, but it's not without its flaws.

For a start, its interface is ungainly and hard to remember, which leads to unsanitary cutting and pasting. That interface also relies on subroutine names stored as strings in package variables. This design imposes all the inherent problems of using package variables, as well as the problems of symbolic references (see Chapters 5 and 11).

It's also redundant: you have to name each subroutine at least twiceonce in its declaration and again in one (or more) of the export lists. And if those disadvantages weren't enough, there's also the ever-present risk of not successfully naming a particular subroutine twice, by misspelling it in one of the export lists.

Exporter also allows you to export variables from a module. Using variables as part of your interface is a bad interface practice (see the following guideline, "Interface Variables"), but actually aliasing them into another package is even worse. For a start, exported variables are ignored by use strict, so they may mask other problems in your code. But more importantly, exporting a module's state variables exposes that module's internal state in such a way that it can be modified without the module's name even appearing in the assignment:

use Serialize ($depth); # and much later... $depth = -20; # Change the internal state of the Serialize module

That's neither obvious, nor robust, nor comprehensible, nor easy to maintain.

To set up a module with a full range of export facilities, including default exports, exports-by-request, and tagged export sets, you have to write something like this:

package Test::Utils; use base qw( Exporter ); our @EXPORT = qw( ok ); # Default export our @EXPORT_OK = qw( skip pass fail ); # By explicit request only our %EXPORT_TAGS = ( ALL => [@EXPORT, @EXPORT_OK], # Everything if :ALL tagset requested TEST => [qw( ok pass fail )], # These if :TEST tagset requested PASS => [qw( ok pass )], # These if :PASS tagset requested ); sub ok {...} sub pass {...} sub fail {...} sub skip {...}

The large amount of infrastructure code required to set up this interface can obscure what's actually being accomplished, which makes it harder to know if what's been accomplished is what was supposed to be accomplished.

A cleaner alternative is to use the Perl6::Export::Attrs CPAN module. With this module there is no separate specification of the export list. Instead, you just annotate the subroutines that you want exported, saying how you want them exported: by default, by request, or as part of particular tagsets.

Using Perl6::Export::Attrs, the export behaviour set up in the previous example could be specified with just:

package Test::Utils; use Perl6::Export::Attrs; sub ok :Export( :DEFAULT, :TEST, :PASS ) {...} sub pass :Export( :TEST, :PASS ) {...} sub fail :Export( :TEST ) {...} sub skip :Export {...}

These annotated definitions specify precisely the same behaviour as the earlier Exporter-based code. Namely that:

  • ok( ) will be exported when requested by name, or when the :TEST or :PASS tagset is requested. It will also be exported by default when no exports are explicitly requested.

  • pass( ) will be exported when requested by name, or when the :TEST or :PASS tagset is requested.

  • fail( ) will be exported when requested by name, or when the :TEST tagset is requested.

  • skip( ) will be exported only when specifically requested by name.

  • Every subroutine marked :Export will automatically be exported if the :ALL tagset is requested

Категории