Build a Small Perl Module with Private State and Explicit Exports
A Perl module that exports too much, or hides state in the wrong place, is a headache six months later when someone else calls it. This walks through a tiny module that keeps its internal state private and only exports what it means to. Examples target Perl 5.38.2, as documented by the installed perlmod page from the perl-doc package.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed Perl
- 2. Create an isolated module directory
- 3. Write a module with one public function
- 4. Load the module and import only what you asked for
- 5. See the difference between package and lexical scope
- 6. Watch load time versus run time
- 7. Test the failure boundary
- 8. Remove the temporary files
Allow about twenty minutes. You need Perl and a shell, everything runs as your normal user in a temporary directory, so nothing here needs elevated privileges. The module is intentionally small: the lesson is where names live, when code loads, and what an export actually changes.
1. Check the installed Perl
Confirm the interpreter before relying on version-specific diagnostics:
$ perl -e 'printf "%vd\n", $^V'
v5.38.2
$ dpkg-query -W -f='${Package} ${Version}\n' perl perl-doc
perl 5.38.2-3.2ubuntu0.6
perl-doc 5.38.2-3.2ubuntu0.6
Your package revision may differ; the behaviour below is based on 5.38.2.
Checkpoint
If perl is missing, stop here and use the normal package-management process for your distribution. Do not use sudo merely to run these examples.
2. Create an isolated module directory
Make a temporary workspace and enter it. This changes only a directory below /tmp:
$ workdir=$(mktemp -d /tmp/perlmod-demo.XXXXXX)
$ cd "$workdir"
$ printf 'workspace: %s\n' "$PWD"
workspace: /tmp/perlmod-demo.XXXXXX
The final directory name is generated by mktemp, so your suffix will differ. Keep the shell open until the checks finish.
Recovery
Once you are done, leave the directory and remove that exact path with rm -rf -- "$workdir". That deletion is irreversible, so do not run it until you no longer need the files.
3. Write a module with one public function
Create Greeting.pm with a package declaration, a private lexical, an explicit export list, and a function:
$ cat > Greeting.pm <<'PERL'
package Greeting;
use strict;
use warnings;
use Exporter qw(import);
our @EXPORT_OK = qw(greet);
my $prefix = 'Hello';
sub greet {
my ($name) = @_;
return "$prefix, $name";
}
1;
PERL
package Greetingputs unqualified dynamic names and subroutine names in theGreetingnamespace for the rest of the file. It does not move lexical variables declared withmy.$prefixis therefore private to this module file.greetis made available for optional import through@EXPORT_OK, and nothing else is exposed.- The final
1;matters. A module loaded withuseorrequiremust return a true value.
The module does not need root access, and it does not install anything system-wide.
4. Load the module and import only what you asked for
Write a caller that adds the current directory to Perl's module search path and requests greet explicitly:
$ cat > demo.pl <<'PERL'
use strict;
use warnings;
use lib '.';
use Greeting qw(greet);
print greet('Ada'), "\n";
print Greeting::greet('Grace'), "\n";
PERL
$ perl demo.pl
Hello, Ada
Hello, Grace
Both calls reach the same module function. The first uses the imported short name; the second uses its fully qualified package name. Prefer the qualified form when it makes ownership clearer, or when you do not want to import a name into the caller at all.
Checkpoint
A clean run prints two lines and no diagnostic. If Perl says it cannot locate Greeting.pm, check that you are still in the temporary directory and that use lib '.' appears before use Greeting.
5. See the difference between package and lexical scope
Change the caller to inspect the public function without reaching into the private lexical:
$ cat > scope-check.pl <<'PERL'
use strict;
use warnings;
no warnings 'once';
use lib '.';
use Greeting ();
print Greeting::greet('Linus'), "\n";
print defined $Greeting::prefix ? "package variable\n" : "no package variable\n";
PERL
$ perl scope-check.pl
Hello, Linus
no package variable
use Greeting () loads the module without importing names, the documented form for making a module available while skipping its import step. The module's my $prefix is lexical, so $Greeting::prefix is not the same value and is not defined by this module at all.
Warning
Do not edit a module's symbol table directly to discover or create state. The manual warns that direct changes to entries which are not already typeglobs have undefined results that can change between Perl releases.
6. Watch load time versus run time
use Module is processed in a BEGIN phase: broadly equivalent to requiring the module and then calling its import method during compilation. Add a small compile-time message to a separate module to make that ordering visible:
$ cat > Phase.pm <<'PERL'
package Phase;
use strict;
use warnings;
BEGIN { print "module compiled\n"; }
sub run { print "subroutine ran\n"; }
1;
PERL
$ perl -I. -e 'use Phase; print "main ran\n"; Phase::run()'
module compiled
main ran
subroutine ran
The BEGIN block runs as soon as it has been compiled, before the rest of the one-line program executes. That is why declarations or imports made by use are already available to later source in the same file.
Tip
Keep module loading free of surprising side effects. A diagnostic, network connection or file change in BEGIN happens before ordinary run-time control reaches the caller, which makes it a strange place to hide something noisy.
7. Test the failure boundary
Requesting a name that is not in @EXPORT_OK should fail during compilation. That is a useful check that your public interface is intentional:
$ perl -I. -MGreeting=not_public -e 'print not_public()'
Undefined subroutine &main::not_public called at -e line 1.
$ printf 'exit status: %s\n' "$?"
exit status: 255
The exact diagnostic location can vary, but the command must fail with a non-zero status. Do not solve this by exporting every internal helper: add a name to @EXPORT_OK only when callers should rely on it, then document or test the resulting interface.
8. Remove the temporary files
When the checks are complete, clean up the exact temporary workspace created earlier:
$ cd /tmp
$ rm -rf -- "$workdir"
$ test ! -e "$workdir" && echo 'temporary workspace removed'
temporary workspace removed
If you need to inspect a failed run, leave the directory in place instead and remove it later after recording the error. None of the examples altered Perl's installed modules, your shell configuration or a service.
Done means
- Confirmed the local Perl and
perl-docversions. - Loaded
Greeting.pmfrom an explicit local search path. - Called
greetboth through an approved import and a qualified call. - Kept the private lexical distinct from the package namespace.
- Watched
useload and import during compilation. - Confirmed an unapproved import fails, and removed the temporary workspace only when safe.