NAME
Exporter::Extensible - Create easy-to-extend modules which export symbols
VERSION
version 0.01
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)
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).
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:
Replace the symbol with no warning.
'warn'
(or'carp'
)Replace the symbol but warn about it using
carp
.'die'
(or'fatal'
or'croak'
)Don't import the symbol, and die by calling
croak
.'skip'
Don't import the symbol and don't warn about it.
- 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 named.
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 => $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.
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= ( ... );
our %EXPORT_TAGS = ( ... );
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 hierarchy of the current package. Note that this lookup happens immediately, and packages derived from this cannot overridefoo
and have that be exported in its place. - '$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. - '-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.
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.
IMPLEMENTING GENERATORS
A generator is just a function that returns the thing to be imported. A generator is called as:
$generator->($exporter, $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 { shift->{foo}= shift },
'-bar(1)' => sub { shift->{bar}= shift },
'=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.
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.
3 POD Errors
The following errors were encountered while parsing the POD:
- Around line 820:
Expected '=item 2'
- Around line 824:
Expected '=item 3'
- Around line 828:
Expected '=item 4'