About 3 weeks ago I posted Module Design strawman - Exporter::VA. Thanks for the comments and feedback. Here is a serious design/user document for it.

The docs were done in POD, and converted to HTML for posting (and then massaged when I discovered how incompatible PM was with regular HTML). I'm interested in some serious, detailed, even picky feedback at this point.

—John



NAME

Exporter::VA - Improved Exporter featuring Versioning and Aliasing.

This is a preliminary design document only.


AUTHOR

John M. Dlugosz


SYNOPSIS

In module ModuleName.pm:

package ModuleName; use Exporter::VA qw/import AUTOLOAD VERSION/;
our %EXPORT= ( # all configuration done in this one hash foo => [ v1.0, 'foo_old', v2.0, 'foo_new' ], bar => 'bar_internal', baz => 'baz', fobble => \&figure_it_out, # parameter list is (caller +, version, symbol, param-list tail) bobble => \\&_boble_internal, $x => '$x', $y => \\$y, # a hard reference rather than a name :tagname => [ 'foo', 'bar' ], :DEFAULT => [ v1.0, ':_old_imports', v2.3, ':_new_impo +rts' ], :_old_imports => [ 'foo', 'bar', 'baz', 'fobble' ], :_new_imports => [ 'fobble' ], .option => "value", .default_VERSION => v1.5, .warnings => 1 );

In other files which wish to use ModuleName:

use ModuleName;
use ModuleName v2.1;
use ModuleName v2.1 qw/ foo bar fobble $y/;


DESCRIPTION

This main incentive in creating this exporter is to allow modules to be updated and get rid of default exports in newer releases, while still maintaining compatibility with older code.

How to Incorporate

The module that wishes to draw upon Exporter::VA for its export needs can simply import the import function. Note that this needs use instead of require, and there is no need to inherit from it.

You can import the import function and Exporter::VA will provide you with one. If you prefer, you can write import yourself to do some enhancements, and then from it call one of the helper functions.

You can also import the AUTOLOAD function, which is an easy way to lazy-generate any aliased functions that are called via module-qualified syntax. There are other ways to do this, described later.

All information for configuring the module's use of Exporter::VA is given in a single hash. By default, this will be the %EXPORT hash found in the calling package.

Or, you can include a hashref as a parameter to the use, and this will specify the destination.

my %Export= ( #... use Exporter::VA (\%Export, 'import'); # look ma, no package +globals!
use Exporter::VA ('import', { foo => \\&internal_foo, bar => \\&internal_bar } ); # put it all inline in this statement.

However it's found, this document will call this information the %EXPORT definition.

Keys in the %EXPORT definition

There are several kinds of keys that may be present in the %EXPORT definition, and they have different purposes and different usage rules.

symbols
If the hash key begins with a Perl sigel or a Perl identifier character (to be exact, /^[$@%*&\w]) it names a symbol that the module's user may import by that name. If there is no sigel, then it assumes a &, so you can say foo instead of &foo.

tags
If the hash key begins with a : character, then it names a list of other names. The module's user may use this tag to import the whole list, as with the traditional Exporter module.

pragmas
If the hash key begins with a - character, then the module user may “import” it to trigger code or special features of the module.

options
If the hash key begins with a . character, it has special meaning to Exporter::VA and is used as an option or parameter to configure the module's use of the exporter.

symbols

If the hash key begins with a Perl sigel or a Perl identifier character (to be exact, /^[$@%*&\w]) it names a symbol that the module's user may import by that name.

The value may be a name, a hard link, a callback, or a version list.

name
If the value is a scalar, then it is the name of the symbol within the package to export. This does not have to be the same as the export name that the module's user will see.
%EXPORT= ( &foo => "foo", # & is optional for functions $bar => "$_internal_bar", baz => "" # means "same". );

As a special case, a an empty string means that the internal name is the same as the export name. foo => 'foo' can also be specified as foo => ''. See also the .plain option.

A value of undef means that the symbol is not available for import. This is the same as leaving it out for a simple name value, but is useful as part of a version list, to indicate that the function was dropped at some point.

non-scalars
If the value is not a scalar or undef, than it is a reference of some kind. This module distinguishes several types based on the kind of reference: array ref is a version list, code ref is a callback, scalar ref is a hard link. Others are errors.

version list
If the value is an array reference, then it specifies alternating v-strings and symbol values.
foo => [ v1.0, 'foo_old', v2.0, 'foo_new' ]

It must begin with a v-string that is the oldest supported version. That is followed by the value to use for that version and later, until it changed with the next named version, and so on. The last value is ``current'' and used up to and including this module's stated $VERSION.

So, in the above example, if the module's user called for:

use ModuleName v1.78 ('foo'); # note no comma after v-string

then ModuleName will export its foo_old function as the caller's foo.

The values in this list in the even positions are the same kinds of values used for symbol name entries, except for another version list. It may be a name, hard link, or callback.

callbacks
If the value is a code reference, then it specifies a callback function. This code will be called when the symbol is being imported, and can do something more complex than the version list allows.
%EXPORT= ( &foo => \&figure_it_out_later, $bar => sub { ... logic goes here ... }, %fible => sub { \%internal_fible } );

When that line is triggered at import time, the code is called with the following parameters:

sub figure_it_out_later { my ($blessed_export_def, $caller, $version, $symbol, $param_l +ist_tail)= @_;

The $caller is the package name of the module's user; that is, the one doing the importing. $version is the v-string of the version he's asking for. $symbol is the name of the symbol he's asking for (a function name will have the & added). Finally, the last argument is an array ref of the rest of the parameter list to import, which this callback may inspect or modify (see pragmas, below).

The function must return a reference to the proper type of thing, which is what will be placed in the caller's package.

hard links
If the %EXPORT definition contains something like &foo => '&foo', then when triggered it will symbolically reference &ModuleName::foo. Instead, you can specify a hard-link, and not even have a name that is visible outside the package (or even the scope!).

In the section on callbacks, the callback %fible => sub { \%internal_fible } ignores the parameters and always returns a reference to the same thing. This is exactly what is meant by a hard-link here. Only there is a shortcut for doing it:

%fible => \\%internal_fible

Basically, use a double-reference to the desired symbol, and that will directly be used at export time as opposed to finding it by name first.

Syntactically, this is a reference to a scalar which must itself contain a reference to the right kind of thing. It is sensible for the normal meaning of the backslash in Perl, adding another layer of deferring things.

tags

If the hash key in the %EXPORT definition begins with a : character, then it names a list of other names. The module's user may use this tag to import the whole list, as with the traditional Exporter module.

The value is a list ref. It may either be a list of names, or a list of alternating v-strings and tags.

A list of names is simply that. The contents of the list replace the tag's name in the import parameter list. So, given

%EXPORT= ( apple => ... banana => ... pear => ... potato => ... cheeze => ... :fruit => [ qw/apple banana pear/ ] );

Then the module's user might say

use ModuleName v1.78 qw/potato :fruit cheeze/;

to mean the exact same thing as

use ModuleName v1.78 qw/potato apple banana pear cheeze/;

The other form is used to version a list. The concept is the same as the version list used for symbol values: The list alternates v-strings and other tag names. The original tag is replaced by the one that matches the desired version, and then processing continues.

For example, given

%EXPORT= ( :list => [ v1.0, ':_old_list', v2.3, ':_new_list' ], :_old_list => [ 'foo', 'bar', 'baz', 'fobble' ], :_new_new => [ 'fobble' ] );

then

use ModuleName v1.2 ':list';

means the same thing as

use ModuleName ':_old_list';

and

use ModuleName v2.4 ':list';

means the same thing as

use ModuleName ':_new_list';

(except that directly importing something that begins with an underscore gives a warning).

the :DEFAULT tag

If there is nothing specified in the import parameter list, then it behaves as if :DEFAULT was specified. This is how you list what gets imported if you don't specify otherwise, and is the equivilent of $EXPORT in the traditional Exporter module.

symbols and tags work together

If a package imports the default in this example, it will note that foo was imported in versions before v2.3 but dropped as a default in that version. But, in version 2.0 the implementation changed, so it will pull in foo_old if the requested version is less than 2, or foo_new if between 2.0 and 2.3, or not import it at all if 2.3 or later.

%EXPORT= ( foo => [ v1.0, 'foo_old', v2.0, 'foo_new' ], :DEFAULT => [ v1.0, ':_old_imports', v2.3, ':_new_impo +rts' ], :_old_imports => [ 'foo', 'bar', 'baz', 'fobble' ], :_new_imports => [ 'fobble' ], # ... others ...

Looking at it step-by step, the module changed what was imported by default. In the traditional system, that's like changing $EXPORT and moving them to $EXPORT_OK instead.

Meanwhile, the implementation of foo changed in version 2.0. The decision of which version of foo to import is independant and orthogonal of the decision of whether foo gets imported by default.

pragmas

If the hash key in the %EXPORT definition begins with a - character, then it defines a pragmatic import.

This is used to trigger a callback, without actually importing anything. For example, given

%EXPORT= ( -prag => \&callme, # ...

then the calling module can say:

use ModuleName v1.0 qw/ foo -prag bar/;

and between doing the work of importing foo and importing bar, it will trigger the function &callme. This is exactly like a callback for a symbol, except that if the symbol name begins with a dash it is thrown away after it resolves it, and doesn't put it into the calling module's package.

Since a callback can see the rest of the import parameter list and modify it, a pragma can take parameters by shifting them off the list.

A pragma might be used to customize the behavior of a module. The module can remember the settings associated with each importer by using a hash keyed by the caller (importer).

%EXPORT= ( -verbose => sub { my $caller= shift; $verbose{$caller} +=1; }

options

If the hash key in the %EXPORT definition begins with a . character, it has special meaning to Exporter::VA and is used as an option or parameter to configure the module's use of the exporter.

Those that begin with a & symbol (after the initial dot) take code references that are invoked just like export callbacks.

.allowed_VERSIONS
The version asked for must be on this list. Normally, version numbers are checked to see if they are between version numbers where things changed, but the exact number doesn't matter. If you specify a list here, then only versions on this list are accepted.
.allowed_VERSIONS => [ 1.0, # initial relase 1.1, # minor fixes

.&begin
This is called just like a symbol callback, before proceeding with processing all the symbols in the import list. Any return value is ignored.
%EXPORT= ( .&begin => sub { my ($blessed_export_def, $caller, $version, $symbol, + $param_list_tail)= @_; #...

.default_VERSION
If the module importer does not specify a version in its use statement, then this value is used. Typically, when switching from Exporter to Exporter::VA to facilitate reducing the list of things exported by default, or versioning a symbol, set the .default_VERSION to the last version before the change. Then, any code that doesn't specify otherwise will get the backward-compatible imports.
.default_VERSION => v1.99, # changed stuff with 2.0, +must ask for new stuff.

If not specified, then a v-string is required in the use statement.

.&end
Just like .&begin, but called after the import list has been all processed.

.plain
This is a list ref that contains symbol names. Before processing begins, everything on it is added to the %EXPORT definition as self-named symbols without versioning or aliases. This makes it easy to have something like the traditional @EXPORT list, copying such a list when upgrading to use Exporter::VA without reformatting it, or just being more succinct.

For example,

%EXPORT= ( .plain => [qw/foo $x &zed],

will generate (recall that an empty string means ``same name as the key'')

%EXPORT= ( &foo => "", $x => "", &zed => "",

.&unknown_import
%EXPORT= ( .&unknown_import => sub { my ($blessed_export_def, $caller, $version, $symbol, + $param_list_tail)= @_; #...

Before generating an error for an unlisted symbol, this callback will be tried. If it returns undef, the error condition continues. If you can resolve it dynamically, return a reference to the correct type of thing based on the name in $symbol.

.&unknown_type
%EXPORT= ( .&unknown_type => sub { my ($blessed_export_def, $caller, $version, $strange +_import_param, $import_list_tail)= @_; #...
If the import list contains something that is not a scalar, then it is + passed to this callback. The thing in question is C<$strange_import_param>.

This is handy for implementing modules that take a hash ref or other object in addition to export names. This can also be done by making it follow a pragmatic import, or looking for it in a .&begin pass.

.warnings
If the value is true, then extra keys that are not known options are reported as warnings, and other possible typos are reported in the %EXPORT definition. If false, then it doesn't go out of its way to look for problems, speeding up the process.

The default value, if this option is not given, is 1. You must specify

.warnings => 0,

to disable these warnings.

user-defined and reserved
To prevent typos, when warnings are enabled, the %EXPORT definition is scanned for unknown options.

If you derive from or otherwise extend Exporter::VA and wish to add more options, use option names beginning with capital letters (or a &amp; followed by a capital letter). All others are reserved for future versions of this module.

Aliases and Non-Imported Calls

An alias specified in the %EXPORT definition only works if it's imported. For example, if the %EXPORT definition contained

foo => [ v1.0, 'foo_old', v2.0, 'foo_new' ], bar => 'bar_internal', fobble => \&figure_it_out,

and the main program used:

use ModuleName v1.5 qw(foo bar);

then the main code could call foo and get ModuleName::foo_old, and call bar and get ModuleName::bar_internal. But what happens if the main code explicitly calls ModuleName::foo or doesn't import at all, and tries to call ModuleName::fobble?

It will indeed attempt to call functions named ModuleName::foo and ModuleName::fobble, respectivly. That is not the same as what happens when calling through the imported symbol.

To handle this, simply arrange it so there is a function defined as ModuleName::foo etc.

The best way to handle this is to assure that the directly-called functions have the identical semantics as the imported aliases.

This can be handled automatically, by having ModuleName import AUTOLOAD from Exporter::VA. Then, a direct call to foo will (if you don't happen to have an unrelated function called foo also!) land in AUTOLOAD, which will automatically generate a suitable ModuleName::foo. It will look up the caller's desired version at run-time and jump to either foo_old or foo_new as appropreate.

If you need to write your own AUTOLOAD for the module for other reasons, you can incorporate this ability by calling the method autoload_symbol.

If you don't like the automatically-generated thunk, you can easily create your own using the underlying helper functions. In order to write a function foo that checks the caller's desired version and calls the appropreate version, use the methods resolve and client_desired_version.

There is no automatic facility to do this for non-functions. You are better off using access methods instead of direct access to data values. But, you can accomplish much the same thing by using ties to a variable of the stated name, where the tie's implementation switches between underlying versions.

If a data structure changes, instead of versioning the export for the data item, have the new version remove the export of the data item and introduce an access method in its place.

Extending Exporter::VA

Using .begin and .end options

The easiest way to add some processing around this module's import semantics is to use the .begin and .end options.

Writing Your Own Import Function

It is simplest to import the implementation of import from Exporter::VA. Anything you can do by wrapping it within a larger piece of code can be done using the .begin and .end options. You can also make changes by overriding various other methods in a derived class. If you do wish to write your own import function, the generated one looks like this:

sub import { my $caller= caller; my $export_def= bless find_export_def($caller,\@_), "Exporter +::VA"; my $version= get_pending_import_version(); $export_def-> set_client_desired_version ($caller, $version); $export_def -> export ($caller, $version, \@_); }

Note that the calls to get_pending_import_version|get_pending_import_version and find_export_def|find_export_def are not ``virtual'', and the call to export is.

See this module's own pragmas for information on customizing this generated code without rewriting it or cutting and pasting it.

Inheriting from Exporter::VA

This module is fundimentally designed to allow custimization via deriving from it. However, the way it is used is unique, and making it behave as an object needs a little explaining.

The import function called by use does not contain a this/self parameter, but only has the list of imports. In order to have all its helper functions make virtual calls and thus allow replacement, and object is introduced as soon as possible.

The object is simply the %EXPORT definition. As you can see from the listing of import above, A reference to the %EXPORT definition is blessed into Exporter::VA, and all subsequent functions are dispatched through it.

If you derive from Exporter::VA, you must bless the object into your derived package instead, and then your function overrides will be used.

Since import has no object, it has the package name hard-coded into it. If you derive from Exporter::VA, you could supply the definition of import from your derived class as well. But if that's the only change you need, you can use this module's own -derived pragma. This is an example of using a pragmatic import to paramiterize the generation of an imported function.

package Extend; use Exporter::VA (); @ISA= ('Exporter::VA'); sub resolve { ... # override what I need to change here
package ModuleName; use Exporter::VA qw/ -derived Extend import AUTOLOAD /; %EXPORT= { ... # # proceed writing my module

Methods

autoload_symbol

autoload_symbol ($blessed_export_def, $module, $symbol)

Call this to implement AUTOLOAD, or pre-generate the thunks. Calling this will generate a sub named $symbol into $module that will redirect to the proper function based on its immediate caller at run-time.

client_desired_version

$vstring= client_desired_version ($blessed_export_def, $caller +)

This returns a v-string stating which version was used by the specified $caller package.

export ($blessed_export_def, $caller, $version, $item_or_list)

This will export a single item or a list of items. $item_or_list can be a single item (as from an import list) or a reference to a whole list. Each item is processed using the full rules of the %EXPORT definition.

You can call this from pragmatic callbacks or other places to explicitly export something at will.

The specified item(s) are resolved and then placed into the $caller's package.

find_export_def ($caller, $import_list)

This is used by import to locate the %EXPORT definition. If it is found in the $import_list, it is removed from the list. If not found in the list, then it looks for the package global %EXPORT in the $caller. Either way, it returns the hash ref.

You only need to call this yourself if implementing your own import.

get_pending_import_version

This is not virtual, and called with no parameters. It returns the v-string last seen by a module starting the import process.

Your module must call Exporter::VA::set_pending_import_version from its sub VERSION function, which is triggered by the version number in a subsequent use statement. So, unless you need to do additional work in VERSION, just import it from Exporter::VA.

Since Perl's VERSION and import functions are somewhat disjoint and don't have any state that is passed between them, this hack is necessary to make it work seamlessly.

resolve

$ref= resolve ($blessed_export_def, $caller, $version, $item)

This will look for $item based on the %EXPORT definition, and return a reference to the thing to export under that name based on the specified $version and possibly the $caller. The $caller is not needed directly, but will be passed to callbacks.

get_pending_import_version

Called by the module's sub VERSION, to record this information since it is not passed to import.

set_client_desired_version ($blessed_export_def, $caller, $version)

Exports

special

If a parameter to Exporter::VA's own import is not a string but a hash ref, then it is taken as the argument to the -def pragma.

functions

import, AUTOLOAD, VERSION

pragmas

-def hashref
-derived name
The pragmatic import -derived name will customize the generated import function to contain the specified name as the package to bless the object into. This is handy for deriving from Exporter::VA. For example:
package ModuleName; use Exporter::VA qw/-derived ExpEnhancement import AUTOLOAD/;

note that this only affects items farther to the right in the list, so it makes sence to always put -derived first.


HISTORY

October 2002 - preliminary documentation and design reviews

Edit by tye to add READMORE tag and change PRE tags to CODE tags.

Replies are listed 'Best First'.
Re: Module Design Review - Exporter::VA
by theorbtwo (Prior) on Oct 29, 2002 at 02:41 UTC

    I have only one potential improvment to offer you: s/sigel/sigil/g. Take that as a complement: it's that good, that the only thing I can find to improve upon is your spelling of obscure occultish ("a sign, word, or device held to have occult power in astrology or magic", m-w.com) words.


    Warning: Unless otherwise stated, code is untested. Do not use without understanding. Code is posted in the hopes it is useful, but without warranty. All copyrights are relinquished into the public domain unless otherwise stated. I am not an angel. I am capable of error, and err on a fairly regular basis. If I made a mistake, please let me know (such as by replying to this node).

Re: Module Design Review - Exporter::VA
by perrin (Chancellor) on Oct 29, 2002 at 05:23 UTC
    Oh great, now every state in the US will want their own exporter!
(tye)Re: Module Design Review - Exporter::VA
by tye (Sage) on Oct 29, 2002 at 15:29 UTC

    Excellent! Here are my mostly minor comments.

    s/sence/sense/, s/\$EXPORT/@EXPORT/, and you need to quote many of your hash keys since things like .foo and &foo are not barewords.

    The function must return a reference to the proper type of thing, which is what will be placed in the caller's package.

    I'd allow for callbacks to return undef to indicate that there is nothing to import so that modules can have non-importing options that use plain names.

    Version strings are going away. You should not be using them in a new module.

            - tye
      Thanks for your notes!

      you need to quote many of your hash keys since things like .foo and &foo are not barewords. Hmm, I guess that's true in general, since the hash keys look exactly like the symbol names! I wonder if that will be a source of annoying user errors, esp. since they may be defined and thus not caught as a warning in the parser!

      I wonder, would a different way of specifying these symbol names be better, or is the clarity of just naming them by what they are normally called make it worth using quotes?

      I'd allow for callbacks to return undef to indicate that there is nothing to import so that modules can have non-importing options that use plain names.

      I originally had that. Someone suggested standardizing on the dash for pragmatic imports. Doing so gives me more error checking. I'm on the fence now about allowing undef where its not expected.

      Version strings are going away. You should not be using them in a new module. So that begs several questions (1) if they are going away, what is taking their place? Some class type that has the same feature of relational operators implemented on it? (2) the use MODULE VERSION (LIST) syntax expects a version string. Is that going away too, or will it be improved, or what?

      By "going away" do you mean Perl6 or something sooner?

      —John

        I'd just quote them.

        You could make erroring out on undef in that case an option. If not, I would have to customize your module in order to use it with my existing module.

        A few months back I read that version strings had been declared a worse abomination than pseudo hashes and had already been deprecated. Searching for verification, that doesn't appear to be the case.

        Update: My existing module is Win32::TieRegistry and a typical use line looks like:

        my $Reg; use Win32::TieRegistry 0.20 ( TiedRef => \$Reg, Delimiter => "/", ArrayValues => 1, qw( :REG_ KEY_ALL_ACCESS ), );
        where options that take an argument are simply recognized by their value, not by having a leading "-". I'd already hoped to use the version argument to disable some default behavior that I had decided was a mistake, so your module would be a welcome aid. (:

                - tye