Search code examples
perlperl-moduleperl-exporter

How to conditionally import functions from another module and export them to the local namespace


Assume I have a module named Local that exports a subroutine subLocal via the %EXPORT_TAGS interface.

This module is closely related to another module named Remote that defines subroutines the user of Local might want to import.

There are two requirements I would like to have:

  1. Module Local should import the subroutines defined in Remote only if the user of module Local is importing a subroutine defined in Remote (either by explicitly naming the export or by using a specific export tag)

  2. When a subroutine from Remote is imported into Local, the user of module Local should be able to refer to that subroutine as if it is in his local namespace (the same behavior you would get when referring to a subroutine defined in Local).

I've only found a (hacky) solution for req. 2 by adding an entry in the symbol table, but this always occurs -- regardless if the user of Local actually needs the subroutines in Remote. According to perldoc, this pointlessly "pollutes" the namespace.

So at what point during compilation or runtime should I be trying to import the subroutines from Remote? And how do I actually import them in such a way that they appear in the local namespace?

This is my current approach. Module Local:

package Local;

use strict;
use warnings;

BEGIN
{
  require Exporter;

  our @ISA = qw| Exporter |;

  our @EXPORT_LOCAL  = qw| subLocal |; 
  our @EXPORT_REMOTE = qw| subRemote |;

  our @EXPORT_OK   = ( @EXPORT_LOCAL, @EXPORT_REMOTE );
  our %EXPORT_TAGS = 
    ( all => \@EXPORT_OK, local => \@EXPORT_LOCAL, remote => \@EXPORT_REMOTE );

  *subRemote  = \&Remote::subRemote; # <-- can I do this conditionally somewhere? 
                                     # <-- and is there a better way to put this function in the user's local namespace?
}

use Remote; # <-- can I do this conditionally somewhere?

sub subLocal { return "(local)" }

1;

And module Remote:

package Remote;

use strict;
use warnings;

BEGIN
{
  require Exporter;

  our @ISA = qw| Exporter |;

  our @EXPORT_REMOTE = qw| subRemote |;

  our @EXPORT_OK   = ( @EXPORT_REMOTE );
  our %EXPORT_TAGS = 
    ( all => \@EXPORT_OK, remote => \@EXPORT_REMOTE );
}

sub subRemote { return "(remote)" }

1;

Solution

  • Why would you want to import subs into Local subs that Local is asked to export? Might as well place them directly into the right module instead of Local!

    Either way, you won't be able to use (just) Exporter. There might an existing alternative to Exporter you could use. Otherwise, you'll need to write your own import.

    Local.pm:

    package Local;
    
    use strict;
    use warnings;
    
    use Carp         qw( croak );
    use Exporter     qw( );
    use Import::Into qw( );
    use Remote       qw( );
    
    my @export_ok_local  = qw( subLocal );
    my @export_ok_remote = qw( subRemote );
    my @export_ok_all    = ( @export_ok_local, @export_ok_remote );
    
    my %export_tags = (
       ':ALL'     => \@export_ok_all,
       ':DEFAULT' => [],
       ':local'   => \@export_ok_local,
       ':remote'  => \@export_ok_remote,
    );
    
    our @EXPORT_OK = @export_ok_local;
    
    sub import {
       my $class = shift;
       my $target = caller;
    
       my @imports =
          map {
             !/^:/
                ? $_
                : !$export_tags{$_}
                   ? croak("\"$_\" isn't a recognized tag")
                   : @{ $export_tags{$_} }
          }
             @_;
    
       my %imports = map { $_ => 1 } @imports;
    
       my @local  = grep { $imports{$_} } @export_ok_local;
       my @remote = grep { $imports{$_} } @export_ok_remote;
    
       delete @imports{ @local, @remote };
       my @unknown = keys(%imports);
       croak("Not exported by ".__PACKAGE__.": @unknown\n") if @unknown;
    
       Remote->import::into($target, @remote);
    
       @_ = ( $class, @local );
       goto &Exporter::import;
    }
    
    sub subLocal { print("subLocal\n"); }
    
    1;
    

    Remote.pm:

    package Remote;
    
    use strict;
    use warnings;
    
    use Exporter qw( import );
    
    our @EXPORT_OK = qw( subRemote );
    
    sub subRemote { print("subRemote\n"); }
    
    1;
    

    Test:

    $ perl -e'
        use Local qw( subLocal subRemote );
        subLocal();
        subRemote();
    '
    subLocal
    subRemote
    

    $ perl -e'
        use Local qw( :ALL );
        subLocal();
        subRemote();
    '
    subLocal
    subRemote
    

    It's far simpler to simply import everything you want to export.

    package Local;
    
    use strict;
    use warnings;
    
    use Exporter qw( import );    
    
    my ( @EXPORT_LOCAL, @EXPORT_REMOTE );
    BEGIN {
      @EXPORT_LOCAL  = qw| subLocal |; 
      @EXPORT_REMOTE = qw| subRemote |;
    
      our @EXPORT_OK = ( @EXPORT_LOCAL, @EXPORT_REMOTE );
    
      our %EXPORT_TAGS = (
        ALL    => \@EXPORT_OK,
        local  => \@EXPORT_LOCAL,
        remote => \@EXPORT_REMOTE,
      );
    }
    
    use Remote @EXPORT_REMOTE;
    
    sub subLocal { ... }
    
    1;