shenme has asked for the wisdom of the Perl Monks concerning the following question:

After a recent discussion (constants in multiple libraries) on moving constants into modules and the benefits thereof, I swore off cut-n-paste and broke out a bunch of constants into a module by themselves, so that they could be shared with some new code.

But I ran into a problem when trying to emulate a CGI.pm feature. Exporter allows you to group together related names for export, giving the set an export 'tag' name. You can then ask for only the groups of names that you really want, e.g.

use Zowie qw( :phasers :masers :tasers !:water_pistols );
CGI.pm permits a short-cut method to export the most useful of its tagged name sets, by asking for ":all". This collects together several export tag sets so that
use CGI qw( :all );
is equivalent to, but much shorter than, saying
use CGI qw( :html2 :html3 :netscape :form :cgi :internal );

But Exporter doesn't understand how to allow you to reference a tag set that contains further references to other tag sets.

I also really liked the CGI.pm idea of listing names for export only once. (Remember I've sworn off cut-n-paste) But Exporter requires that you list all exported names in @EXPORT or @EXPORT_OK. So names I've already listed in %EXPORT_TAGS would have to be duplicated to avoid errors.

It turns out that CGI.pm has quite a bit of magic to do these things. The few other common modules that do something similar also must use magic. And while now the Perl 5.8.x Exporter documentation mentions workarounds to get nearly the same effect, that code doesn't quite do everything I want.

I wanted to be able to

Here is some code for discussion:

package LISConst; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); require Exporter; @ISA = qw(Exporter); $VERSION = '0.01'; @EXPORT = qw(); @EXPORT_OK = qw(); %EXPORT_TAGS = ( hdr_lens => [qw( LIS_TIF_HDR_LEN LIS_TIF_HDR_DATA_MAX LIS_PHY_HDR_LEN LIS_LR_HDR_LEN )], phyhdr_flags => [qw( . . . . )], lr_types => [qw( . . . . )], . . . . all => [qw( ? ? ? ? )], );

I've already listed names in the tag groups in %EXPORT_TAGS and I don't want to have to duplicate list them again in @EXPORT_OK. But Exporter will error with "LIS_TIF_HDR_LEN" is not exported by the LISConst module if I don't also get these names into @EXPORT_OK.

And what I really want for the ':all' tag set is

all => [qw( :hdr_lens :phyhdr_flags :lr_types )],
and end up with Exporter understanding that I'm exporting all the names within the referenced tag sets. Plus I'd like to be able use regular names mixed in with the set names.

After looking at the various bits of library code plus the updated documentation in Perl 5.8.x I came up with this:

{ # Expand tag names into referenced name sets my %seen; $EXPORT_TAGS{all} = [ grep { !$seen{$_}++ } map { m/^:(.*)$/ ? @{$EXPORT_TAGS{$1}} : $_ } @{$EXPORT_TAGS{all}} ]; } { # Add tag set names not already listed into @EXPORT_OK my %seen; $seen{$_}++ foreach @EXPORT_OK; push @EXPORT_OK, grep { !$seen{$_}++ } @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS; }

Have y'all seen other code like this, or perhaps a better (more insightful) solution to the problem? This seems to do what I want, but sure looks oogly.

Replies are listed 'Best First'.
Re: How to do "use Mumble ':all' " ala CGI.pm
by fergal (Chaplain) on Sep 20, 2004 at 09:35 UTC
    I wanted the same thing so I wrote Exporter::Easy. It's a frontend to Exporter, it allows you to do
    use strict; package MyModule; use Exporter::Easiest qw( :base => $base1 $base2 :extra => $extra1 $extra2 :bonus => $bonus1 $bonus2 :useful => $extra1 $bonus1 :useless => :extra :bonus !:useful :no_bonus => :all !:bonus EXPORT => :base ALL => all OK => $not_exported_by_default ); $base1 = 1; $base2 = 2; ...
    this code
    • sets up @EXPORT, @EXPORT_OK and %EXPORT_TAGS
    • makes the class inherit from Exporter
    • defines a tag called :all that represents all the symbols
    • does a use vars for you on the variables that occur in the export spec.

    I think this does everything you're looking for.

      I was going to reply to ikegami's posting that "surely somebody had done something like this before", and you had! I don't know how I missed the module on search.cpan.org when searching on the word 'exporter'. It pops up blinking now.

      It looks like the syntax

      use Exporter::Easiest q( :hdr_lens => LIS_TIF_HDR_LEN LIS_TIF_HDR_DATA_MAX LIS_PHY_HDR_LEN LIS_LR_HDR_LEN :phyhdr_flags => . . . . :lr_types => . . . . . . . . :all => :hdr_lens :phyhdr_flags :lr_types . . . . );
      would have gotten me what I wanted. And I like the idea of automatically doing the "use vars" on mentioned variables. Thanks!
Re: How to do "use Mumble ':all' " ala CGI.pm
by Zaxo (Archbishop) on Sep 20, 2004 at 05:06 UTC

    You could leave off the 'all' key at first, and then set it up with the values from %EXPORT_TAGS,

    %EXPORT_TAGS = ( hdr_lens => [qw( LIS_TIF_HDR_LEN LIS_TIF_HDR_DATA_MAX LIS_PHY_HDR_LEN LIS_LR_HDR_LEN )], phyhdr_flags => [qw( . . . . )], lr_types => [qw( . . . . )], # . . . . ); $EXPORT_TAGS{'all'} = [map {@$_} values %EXPORT_TAGS];

    After Compline,
    Zaxo

      Hmm, yes, but then I wouldn't be getting the same effect as CGI.pm aims for. Instead of every tag set (like the Exporter 5.8.x workarounds suggest), rather allowing you to specify just the most useful subset of all tag sets. Perhaps a better example from CGI.pm would be the ':standard' tag set.

        Ok, if you want to aggregate a few tags in a more general class, use a slice,

        $EXPORT_TAGS{'standard'} = [map {@$_} @EXPORT_TAGS{qw/the top tags/}];

        After Compline,
        Zaxo

Re: How to do "use Mumble ':all' " ala CGI.pm
by ikegami (Patriarch) on Sep 20, 2004 at 05:14 UTC
    You could create a class that inherits from Exporter to do the magic, thus hiding the "oogliness".
    package MyExporter; use Exporter (); BEGIN { our @ISA = 'Exporter'; } sub import { our @EXPORT_OK; our @EXPORT_TAGS; #my $pkg = \%::; #$pkg = $pkg->{$_} foreach ((caller().'::') =~ /\G(.*::)/g); my $pkg = caller() . '::'; { no strict 'refs'; $pkg = *$pkg; } local *EXPORT_OK = $pkg->{'EXPORT_OK'}; local *EXPORT_TAGS = $pkg->{'EXPORT_TAGS'}; { # Expand tag names into referenced name sets my %seen; $EXPORT_TAGS{all} = [ grep { !$seen{$_}++ } map { m/^:(.*)$/ ? @{$EXPORT_TAGS{$1}} : $_ } @{$EXPORT_TAGS{all}} ]; } { # Add tag set names not already listed into @EXPORT_OK my %seen; $seen{$_}++ foreach @EXPORT_OK; push @EXPORT_OK, grep { !$seen{$_}++ } @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS; } &Exporter::import; }

    Then your module becomes:

    package LISConst; use vars qw($VERSION @ISA %EXPORT_TAGS); require MyExporter; @ISA = qw(MyExporter); $VERSION = '0.01'; %EXPORT_TAGS = ( hdr_lens => [qw( LIS_TIF_HDR_LEN LIS_TIF_HDR_DATA_MAX LIS_PHY_HDR_LEN LIS_LR_HDR_LEN )], phyhdr_flags => [qw( . . . . )], lr_types => [qw( . . . . )], );

      Even better, a version that expands ':' deeply (Update:) AND handles !:tag...

      package MyExporter; use Exporter (); BEGIN { our @ISA = 'Exporter'; } sub import { our @EXPORT; our @EXPORT_OK; our @EXPORT_TAGS; #my $pkg = \%::; #$pkg = $pkg->{$_} foreach ((caller().'::') =~ /\G(.*::)/g); my $pkg = caller() . '::'; { no strict 'refs'; $pkg = *$pkg; } local *EXPORT = $pkg->{'EXPORT'}; local *EXPORT_OK = $pkg->{'EXPORT_OK'}; local *EXPORT_TAGS = $pkg->{'EXPORT_TAGS'}; $EXPORT_TAGS{'all'} = [ @EXPORT, @EXPORT_OK, @EXPORT_TAGS ]; { # Expand :tag into components, deeply. # Cycles are silently broken in an arbitrary manner. my %expanded; my $expand = sub { my ($key) = @_; return if ($expanded{$key}++); my %list; foreach (@{$EXPORT_TAGS{$key}}) { if (/^!(:.*)/) { my $tag = $1; &$expand($tag); delete(@list{@{$EXPORT_TAGS{$tag}}}); } elsif (/^:.*/) { my $tag = $_; &$expand($tag); %list = (%list, map { $_ => 1 } @{$EXPORT_TAGS{$tag}}); } else { $list{$_} = 1; } } @{$EXPORT_TAGS{$key}} = keys(%list); }; &$expand($_) foreach (keys(%EXPORT_TAGS)); } &Exporter::import; }

      (untested)

Re: How to do "use Mumble ':all' " ala CGI.pm
by ysth (Canon) on Sep 20, 2004 at 06:39 UTC
    What you have looks reasonable to me.

    From a module-consumer perspective, I sometimes use Exporter's regex feature to get all the exports from a module that neglects to set up an :all tag, e.g. use Foo "/./";. This tripped me up once with a general debugging script that did -MScalar::Util=/./ when isvstring was added. It got trapped by export_fail prior to 5.8.0 and just croaked :(

    $ perl5.6.2 -MScalar::Util=/./ -we0 Vstrings are not implemented in the version of perl at -e line 0
      Yes, I'd thought of just using something like /^LIS_/, as in this case all the interesting constants began with just that. But I'd been so intrigued with the CGI.pm widget that I went off on this tangeant.