NAME
Exporter::Extensible - Create easy-to-extend modules which export symbols
VERSION
version 0.04
SYNOPSIS
Define a module with exports
package My::Utils;
use Exporter::Extensible -exporter_setup => 1;
export(qw( foo $x @STUFF -strict_and_warnings ), ':baz' => ['foo'] );
sub foo { ... }
sub strict_and_warnings {
strict->import;
warnings->import;
}
Create a new module which exports all that, and more
package My::MoreUtils;
use My::Utils -exporter_setup => 1;
sub util_fn3 : Export(:baz) { ... }
Use the module
use My::MoreUtils qw( -strict_and_warnings :baz @STUFF );
# Use the exported things
push @STUFF, foo(), util_fn3();
DESCRIPTION
As a module author, you have dozens of exporters to choose from, so I'll try to get straight to the pros/cons of this module:
Pros
- Extend Your Module
-
This exporter focuses on the ability and ease of letting you "subclass" a module-with-exports to create a derived module-with-exports. It supports multiple inheritance, for things like tying together all your utility modules into a mega-utility module.
- Extend Behavior of
import
-
This exporter supports lots of ways to add custom processing during 'import' without needing to dig into the implementation.
- More than just subs
-
This module supports exporting
foo
,$foo
,@foo
,%foo
, or even*foo
. It also supports tags (:foo
) and options (-foo
). - Be Lazy
-
This exporter supports on-demand generators for symbols, as well as tags! So if you have a complicated or expensive list of exports you can wait until the first time each is requested before finding out whether it is available or loading the dependent module.
- Full-featured
-
This exporter attempts to copy useful features from other popular exporters, like renaming imports with
-prefix
/-suffix
/-as
, excluding symbols with-not
, scoped-unimport, passing options to generators, importing to things other thancaller
, etc. - More-Than-One-Way-To-Declare-Exports
-
Pick your favorite. You can use the export do-what-I-mean function, method attributes, the
__PACKAGE__->exporter_ ...
API, or declare package variables similar to Exporter. - No Non-core Dependencies
-
Because nobody likes big dependency trees.
- Speed
-
I haven't benchmarked this yet, but I approached it with a mindset of "make the common case fast". The features are written so you only pay for what you use.
Cons
- Imposes meaning on hashrefs
-
If the first argument to
import
is a hashref, it is used as configuration ofimport
. Hashref arguments following a symbol name are treated as arguments to the generator (if any) or config overides for a tag. (but you can control hashref processing on your own for-NAME
in the API you are authoring). - Imposes meaning for notation
-NAME
-
This module follows the Exporter convention for symbol names but with the additional convention that names beginning with dash
-
are treated as requests for runtime behavior. Additionally, it may consume the arguments that follow it, at the discresion of the module author. This feature is designed to feel like command-line option processing. - (Small) Namespace and inheritance pollution
-
This module defines an API used for the declaration and implementation of the export process. If you wanted to export any symbol starting with the prefix
exporter_
then you probably shouldn't build on top of this exporter. (I could have kept these meta-methods in a separate namespace, but that would defeat the goal of "easy to extend".)If you want a pure class hierarchy but also export a few symbols, consider something like this:
package My::Class; package My::Class::Exports { use Exporter::Extensible -exporter_setup => 1; ... } sub import { My::Class::Exports->import_into(scalar caller, @_) }
IMPORT API (for consumer)
import
When you call use MyPackage @list
it is equivalent to
BEGIN {
require MyPackage;
MyPackage->import(@list)
}
The user-facing API is mostly the same as Sub::Exporter or Exporter::Tiny, except that -foo
is not a group and there are no "collections" (though you could implement collections using options).
The elements of @list
are handles as:
name
, $name
, @name
, %name
, *name
, :name
Same as Exporter, except it might be generated on the fly, and may be followed by an options hashref.
-name
Run custom processing defined by module author, possibly consuming arguments that follow it.
{...}
(Global Options)
If the first argument to import
is a hashref, these fields are recognized:
- into
-
Package name or hashref to which all symbols will be exported. Defaults to
caller
. - scope
-
Empty scalar-ref whose scope determines when to unimport the things just imported. After a successful import, this wil be assigned a scope-guard object whose destructor un-imports those same symbols. This saves you the hassle of calling "no MyModule @args".
{ use MyModule { scope => \my $scope }, ':sugar_methods'; # use sugar methods ... # you could "undef $scope" if you want them removed sooner } # All those symbols are now neatly removed from your package
- not
-
Only applies to tags. Can be a scalar, regex, coderef, or list of any of those that filters out un-wanted imports.
use MyModule ':foo' => { -not => 'log' }; use MyModule ':foo' => { -not => qr/^log/ }; use MyModule ':foo' => { -not => sub { $forbidden{$_} } }; use MyModule ':foo' => { -not => [ 'log', qr/^log/, sub { ... } ] };
- no
-
If true, then the list of symbols will be uninstalled from the
into
package. For example,no MyModule @args
is the same asMyModule->import({ no => 1 }, @args)
- replace
-
Determines what to do if the symbol already exists in the target package:
- installer
-
A coderef which will be called instead of "exporter_install" or "export_uninstall". Uses the same arguments:
installer => sub { my ($exporter, $list)= @_; for (my $i= 0; $i < @$list; $i+= 2) { my ($name, $ref)= @{$list}[$i..1+$i]; ... }
- prefix (or -prefix)
-
Prefix all imported names with this string.
- suffix (or -suffix)
-
Append this string to all imported names.
NAME => { ... }
(In-line Options)
The arguments to import
are generally scalars. If one is followed by a hashref, the hashref becomes the argument to the generator (if any), but may also contain:
- -as => $name
-
Install the thing as this exact name. (no sigil, but relative to
into
) - -prefix
-
Same as global option
prefix
, limited to this one tag. - -suffix
-
Same as global option
suffix
, limited to this one tag. - -not
-
Same as global option
not
, limited to this one tag. - -replace
-
Same as global option
replace
, limited to this one tag.
import_into
When you call use MyModule @list
you are calling require MyModule; MyModule->import(@list)
. It automatically picks up the calling package, and imports there. As a shortcut for the into
option, you may say MyModule->import_into("SomePackage", @list)
.
There is also a more generic way to handle this need though - see Import::Into
EXPORT API (for author)
The underlying requirements for using this exporter are to inherit from it, and declare your exports in the variables %EXPORT
and %EXPORT_TAGS
. The quickest way to do that is:
package My::Module;
use Exporter::Extensible -exporter_setup => 1;
export(...);
Those lines are shorthand for:
package My::Module;
use strict;
use warnings;
use parent 'Exporter::Extensible';
our (@EXPORT, %EXPORT, %EXPORT_TAGS);
$EXPORT_TAGS{default} ||= \@EXPORT;
$EXPORT{...}= ...; # for each argument to export()
Everything else below is just convenience and shorthand to make this easier.
Export by API
This module provides an api for specifying the exports. You can call these methods on __PACKAGE__
, or if you ask for version-1 as -export_setup => 1
you can use the convenience function "export".
export
This function takes a list of keys (which must be scalars), with optional values which must be refs. If the value is omitted, export
attempts to do-what-you-mean to find it.
foo => \&CODE
-
This declares a normal exported function. If the ref is omitted,
export
looks for it in the the current package. Note that this lookup happens immediately, so derived packages that want to overridefoo
must re-declare it. '$foo' => \$SCALAR
,'@foo' => \@ARRAY
,'%foo' => \%HASH
,'*foo' => \*GLOB
-
This exports a normal variable or typeglob. If the ref is omitted,
export
looks for it in the current package. Note: this exports a global variable which can be modified. In general, that's bad practice, but might be desired for efficiency. If your goal is efficient access to a singleton object, consider a generator instead like=$foo
. -foo => $CODEREF
or-foo => \"methodname"
-
This differs from a normal exported function in that it will execute the coderef at import time, and sub-packages can override it, since it gets called as a method. The default is to derive the method name by removing the
-
. ':foo' => \@LIST
-
Declaring a tag is nothing special; just give it an arrayref of what should be imported when the tag is encountered.
'=$foo' => $CODEREF
or'=$foo' => \"methodname"
-
Prefixing an export name with an equal sign means you want to generate the export on the fly. The ref is understood to be the coderef or method name to call (as a method) which will return the ref of the correct type to be exported. The default is to look for
_generate_foo
,_generateScalar_foo
,_generateArray_foo
,_generateHash_foo
, etc.
exporter_register_symbol
__PACKAGE__->exporter_register_symbol($name_with_sigil, $ref);
exporter_register_option
__PACKAGE__->exporter_register_option($name, $method, $arg_count);
This declares an "option" like -foo
. The name should not include the leading -
. The $method
argument can either be a package method name, or a coderef. The $arg_count
is the number of options to consume from the import(...)
list following the option.
To declare an option that consumes a variable number of arguments, specify *
for the count and then write your method so that it returns the number of arguments it consumed.
exporter_register_generator
__PACKAGE__->exporter_register_generator($name_with_sigil, $method);
This declares that you want to generate $name_with_sigil
on demand, using $method
. $name_with_sigil
may be a tag like ':foo'
. $method
can be either a coderef or method name. The function will be called as a method on an instance of your package. The instance is the blessed hash of options passed by the current consumer of your module.
exporter_register_tag_members
__PACKAGE__->exporter_register_tag_members($tag_name, @members);
This pushes a list of @members
onto the end of the named tag. $tag_name
should not include the leading ':'. These @members
are cumulative with tags inherited from parent packages. To avoid inheriting tag members, register a generator for the tag, instead.
Export by Attribute
Attributes are fun. If you enjoy artistic code, you might like to declare your exports like so:
sub foo : Export( :foo ) {}
sub bar : Export(-) {}
sub _generate_baz : Export(= :foo) {}
instead of
export( 'foo', '-bar', '=baz', ':foo' => [ 'foo','baz' ] );
The notations supported in the Export
attribute are different but similar to those in the "export" function. You may include one or more of the following in the parenthesees:
foo
-
This indicates the export-name of a sub. A sub may be exported as more than one name. Note that the first name in the list becomes the official name (ignoring the actual name of the sub) which will be added to any tags you listed.
:foo
-
This requests that the export-name get added to the named tag. You may specify any number of tags.
-
,-(N)
,-foo
,-foo(N)
-
This sets up the sub as an option, capturing N arguments. In the cases without a name, the name of the sub is used. N may be
'*'
or'?'
; see "IMPLEMENTING OPTIONS". =
,=$
,=@
,=%
,=*
,=foo
,=$foo
, ...-
This sets up the sub as a generator for the export-name. If the word portion of the name is omitted, it is taken to be the sub name minus the prefix "_generate_" or "_generate$REFTYPE_". See "IMPLEMENTING GENERATORS".
Export by Variables
As shown above, the configuration for your exports is the variable %EXPORT
. If you want the fastest possible module load time, you might decide to populate %EXPORT
manually.
The keys of this hash are the strings that the user would specify as the import
arguments, like '-foo'
, '$foo'
, etc. The value should be some kind of reference matching the sigil. Functions should be a coderef, scalars should be a scalarref, etc. But, there are two special cases:
- Options
-
An option is any key starting with
-
, like this module's own-exporter_setup
. The values for these must be a pair of[ $method_name, $arg_count_or_star ]
. (the default structure is subject to change, but this notation will always be supported){ '-exporter_setup' => [ "exporter_setup", 1 ] }
This means "call
$self->exporter_setup($arg1)
when you seeimport('-exporter_setup', $arg1, ... )
. Because it is a method call, subclasses of your module can override it. - Generators
-
Sometimes you want to generate the thing to be exported. To indicate this, use a ref-ref of the method name, or a ref of the coderef to execute. For example:
{ foo => \\"_generate_foo", bar => \\&generate_bar, baz => \sub { ... }, }
Again, this is subject to change, but these notations will always be supported for backward-compatibility.
Meanwhile the %EXPORT_TAGS
variable is almost identical to the one used by Exporter, but with a few enhancements:
:all
-
You don't need to declare the tag
all
, because this module calculates it for you, from the list of all keys of%EXPORT
excluding tags or options. You can override this default though. :default
-
@EXPORT
is added to%EXPORT_TAGS
as'default'
. So, you can push items into@EXPORT
or into@{$EXPORT_TAGS{default}}
and it is the same arrayref. - Resetting the Tag Members
-
If the first element of the arrayref is
undef
, it means "don't inherit the tag members from the parent class".# Don't want to inherit members of ':foo' from parent: foo => [ undef, 'x', 'y', 'z' ]
- Data in a Tag
-
The elements of a tag can include parameters to generators, or arguments to an option, etc; anything that could be passed to
import
will work as expected.foo => [ 'x', 'y', -init => [1,2,3] ],
- Generators
-
If the value in
%EXPORT_TAGS
is not an arrayref, then it should be a REF-ref of either the scalar name of a generator, or a coderef of the generator.foo => \\"_generate_foo",
IMPLEMENTING OPTIONS
Exporter::Extensible lets you run whatever code you like when it encounters "-name" in the import list. To accomodate all the different ways I wanted to use this, I decided to let the option decide how many arguments to consume. So, the API is as follows:
# By default, no arguments are captured. A ref may not follow this option.
sub name : Export( -name ) {
my $exporter= shift;
...
}
# Ask for three arguments (regardless of whether they are refs)
sub name : Export( -name(3) ) {
my ($exporter, $arg1, $arg2, $arg3)= @_;
...
}
# Ask for one argument but only if it is a ref of some kind.
# If it is a hashref, this also processes import options like -prefix, -replace, etc.
sub name : Export( -name(?) ) {
my ($exporter, $maybe_arg)= @_;
...
}
# Might need any number of arguments. Return the number we consumed.
sub name : Export( -name(*) ) {
my $exporter = shift;
while (@_) {
last if ...;
...
++$consumed;
}
return $consumed;
}
The first argument $exporter
is a instance of the exporting package, and you can inspect it or even reconfigure it. For instance, if you want your option to automatically select some symbols as if they had been passed to "import", you could call "exporter_also_import".
exporter_also_import
This method can be used *during* a call to import
for an option or generator to request that aditional things be imported into the caller as if the caller ad requested them on the import line. For example:
sub foo : Export(-) {
shift->exporter_also_import(':all');
}
This causes the option -foo
to be equivalent to the tag ':all'
;
IMPLEMENTING GENERATORS
A generator is just a function that returns the thing to be imported. A generator is called as:
$exporter->$generator($symbol, $args);
where $exporter
is an instance of your package, $symbol
is the name of the thing as specified to import
(with sigil) and $args
is the optional hashref the user might have given following $symbol
.
If you wanted to implement something like Sub::Exporter's "Collectors", you can just write some options that take an argument and store it in the $exporter instance. Then, your generator can retrieve the values from there.
package MyExports;
use Exporter::Extensible -exporter_setup => 1;
export(
# be sure to use names that won't conflict with Exporter::Extensible's internals
'-foo(1)' => sub { $_[0]{foo}= $_[1] },
'-bar(1)' => sub { $_[0]{bar}= $_[1] },
'=foobar' => sub { my $foobar= $_[0]{foo} . $_[0]{bar}; sub { $foobar } },
);
package User;
use MyModule -foo => 'abc', -bar -> 'def', 'foobar', -foo => 'xyz', 'foobar', { -as => "x" };
# This exports a sub as "foobar" which returns "abcdef", and a sub as "x" which
# returns "xyzdef". Note that if the second one didn't specify -as, it would get ignored
# because 'foobar' was already queued to be installed.
AUTOLOADING SYMBOLS AND TAGS
In the same spirit that Perl lets you AUTOLOAD methods on demand, this exporter lets you define symbols and tags on demand. Simply override one of these methods:
exporter_autoload_symbol
my $ref= $self->exporter_autoload_symbol($sym);
This takes a symbol (including sigil), and returns a ref which should be installed. The ref is cached, but not added to the package %EXPORT
. If you want that to happen, you need to do it yourself. This method is called once at the end of iterating the package hierarchy, so you should call next::method
if you don't recognize the symbol.
exporter_autoload_tag
my $arrayref= $self->exporter_autoload_tag($name);
This takes a tag name (no sigil) and returns an arrayref of items which should be added to the tag. The combined tag members are cached, but not added to the package %EXPORT_TAGS
. This method is called only if no package in the hierarchy defined the tag, which could cause confusion if a derived class wants to add a few symbols to a tag which is otherwise autoloaded by a parent. This method is called once at the end of iterating the package hierarchy, so you should call next::method
to collect any inherited autoloaded members of this tag.
SEE ALSO
AUTHOR
Michael Conrad <mike@nrdvana.net>
COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Michael Conrad.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.