Perl Part I · Module 12 · Modules and Packages
Why Code Belongs in a Module
Eleven modules of this track have been about writing Perl inside one file. That is the right shape for a script you run once, and it stops being the right shape the moment a second script needs the same logic. The usual answer at that point is to copy the subroutine, which works perfectly until somebody improves one of the copies — and the measurement in section one is what that costs: two reports, run against the same three lines of input, disagreeing about which path is the worst in the design. Neither script errors. Both exit zero. Both numbers are real. A module removes the possibility of that disagreement by making one definition the only definition. What it gives you in return is a bill: the names a module offers become an interface that other files depend on, and changing them is no longer a local edit. This chapter is about both halves of that trade, because taking the first without understanding the second is how a shared library becomes the thing nobody dares touch.
Intermediate12 min readPerlPart IModules
Part I · Module 12 · Chapter 12.1 · Why Code Belongs in a Module
1. What Copied Code Costs
Here is one parsing rule, copied into two reporting scripts. One copy was later widened to accept a slack value written without its leading zero; the other was not.
use strict;
use warnings;
# The same rule, copied into two scripts. One copy was later "improved" to
# accept a value written without its leading zero. The other was not.
sub parse_in_report_a {
my ($line) = @_;
my ($pin, $slack) = $line =~ m{^(\S+)\s+(-?\d+\.\d+)$};
return defined $pin ? { pin => $pin, slack => $slack + 0 } : undef;
}
sub parse_in_report_b {
my ($line) = @_; # the "improved" copy
my ($pin, $slack) = $line =~ m{^(\S+)\s+(-?\d*\.?\d+)$};
return defined $pin ? { pin => $pin, slack => $slack + 0 } : undef;
}
# The row that only ONE copy understands is also the WORST path in the run.
my @lines = ("u_alu/A -0.12", "u_fifo/B -.95", "u_regf/C 0.44");
my @a = grep { defined } map { parse_in_report_a($_) } @lines;
my @b = grep { defined } map { parse_in_report_b($_) } @lines;
printf "lines in : %d\n", scalar @lines;
printf "script A parsed : %d\n", scalar @a;
printf "script B parsed : %d\n", scalar @b;
my ($worst_a) = sort { $a->{slack} <=> $b->{slack} } @a;
my ($worst_b) = sort { $a->{slack} <=> $b->{slack} } @b;
printf "A worst path : %-9s %s\n", $worst_a->{pin}, $worst_a->{slack};
printf "B worst path : %-9s %s\n", $worst_b->{pin}, $worst_b->{slack};
printf "same answer : %s\n", ($worst_a->{pin} eq $worst_b->{pin} ? "yes" : "NO");lines in : 3
script A parsed : 2
script B parsed : 3
A worst path : u_alu/A -0.12
B worst path : u_fifo/B -0.95
same answer : NOTwo scripts, one run, two different worst paths.
Script A never saw the u_fifo/B -.95 row at all, because its pattern requires a digit before the decimal point. So it reported the worst path as u_alu/A at -0.12 — a real pin with a real number, which is exactly why nobody questions it. Script B saw all three rows and correctly reported u_fifo/B at -0.95, almost eight times worse.
Nothing failed. Both scripts exited cleanly. The only symptom is that two reports about the same design disagree, and the person reading them has no way to tell which is right.
2. One Definition
Here is the same work with the rule in a module. The module's source is shown first; sections of Chapter 12.5 build this file properly.
package VLSI::Log;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT_OK = qw(parse_slack worst_slack);
sub parse_slack {
my ($line) = @_;
my ($pin, $slack) = $line =~ m{^(\S+)\s+(-?[\d.]+)\s*$};
return defined $pin ? { pin => $pin, slack => $slack + 0 } : undef;
}
sub worst_slack {
my (@rows) = @_;
my $worst;
for my $r (@rows) {
next unless defined $r;
$worst = $r if !defined $worst || $r->{slack} < $worst->{slack};
}
return $worst;
}
1;And the script that uses it:
use strict;
use warnings;
use lib 'lib';
use VLSI::Log qw(parse_slack worst_slack);
# Both reports now call the SAME code, so they cannot disagree.
my @lines = ("u_alu/A -0.12", "u_fifo/B -.95", "u_regf/C 0.44");
my @rows = map { parse_slack($_) } @lines;
my $report_a = worst_slack(@rows);
my $report_b = worst_slack(@rows);
printf "parsed : %d of %d\n", scalar(grep { defined } @rows), scalar @lines;
printf "A worst path : %-9s %s\n", $report_a->{pin}, $report_a->{slack};
printf "B worst path : %-9s %s\n", $report_b->{pin}, $report_b->{slack};
printf "same answer : %s\n", ($report_a->{pin} eq $report_b->{pin} ? "yes" : "no");
print "\nwhatever the module's rule is, both reports now share it\n";parsed : 3 of 3
A worst path : u_fifo/B -0.95
B worst path : u_fifo/B -0.95
same answer : yes
whatever the module's rule is, both reports now share itBoth reports agree, because there is nothing left for them to disagree about.
Note the careful wording of the last line of output. The module's pattern is (-?[\d.]+), which is the alphabet check Chapter 11.1 measured and found wanting — it would accept .... as a number. Moving code into a module did not make it correct. What it did was make the rule single-valued: whatever it accepts, every caller accepts the same thing, and fixing it is one edit in one place.
That distinction is worth being precise about, because "put it in a module" is often offered as though it were a correctness measure. It is a consistency measure. Correctness is still your job, and a module makes it a job you only have to do once.
3. What You Gain, and What You Then Owe
One definition, and the interface it creates
The gain is the top half of that figure: two callers, one definition, no possibility of drift.
The obligation is the bottom half, and it is easy to miss because nothing announces it. The moment parse_slack is a name that other files write down, it has become an interface. Renaming it, changing what it returns, or changing which rows it accepts is no longer an edit to your file — it is a change to every file that calls it, including ones written by people who will not read your commit message.
Three practical consequences follow, and they are the actual content of "what you owe it":
Keep the exported set small and deliberate. Every name a module offers is a promise. Chapter 12.6 is about the mechanism — @EXPORT_OK rather than @EXPORT — but the judgement is here: export the two or three things callers genuinely need, and leave the helpers unexported.
Return a shape, not a convenience. parse_slack returns a hash reference with named fields. A version that returned a two-element list would be shorter to write and would break every caller the day you add a third field. Chapter 10.8 measured the same point for subroutines; for a module it matters more, because the callers are not in front of you.
Make failure explicit. The module returns undef for a line it cannot parse, and section two's script counts how many it got. A module that silently returned an empty hash for a bad line would push the cost onto every caller.
4. When To Move Code Out
The canonical rule for this chapter is sharp and worth quoting as it stands: a script should become a module as soon as a second script needs the same code. Not before.
"Not before" is the part people get wrong in the other direction. A single script with a well-named subroutine in it is easier to read, easier to run, and easier to change than the same code split across a script and a module directory — you can see all of it at once, and there is no @INC to configure. Splitting early buys you nothing and costs you the indirection.
The signals that the moment has arrived are concrete:
- You are about to copy a subroutine into a second file. This is the canonical trigger, and section one is why.
- You have already copied it, and the copies have started to differ. Then the module is not a refactor, it is a bug fix.
- A third script wants it, which means the thing is a shared definition of a format, not a helper.
- You want to test the logic on its own, without running the whole report.
And the signals that it has not:
- The code is used once and is unlikely to be used again.
- It is five lines and would need more lines of module boilerplate than it has content.
- You are splitting it to "be tidy" rather than because a second caller exists.
Chapter 12.5 builds the module file itself, and Chapter 11.22's advice applies here too: the fact that a tool exists is not a reason to use it on work that does not need it.
5. What To Remember
- Copied code drifts, and nothing can detect it. Measured: two scripts with copies of one rule reported
u_alu/A -0.12andu_fifo/B -0.95as the worst path from the same three lines. Both exited 0. - The fix is one definition, not better discipline. There is no test that fails when two subroutines in two files stop matching.
- A module is a consistency measure, not a correctness measure. The shared rule here still accepts values no sane parser should — but it accepts them identically everywhere, and fixing it is one edit.
- Moving code out creates an interface. The exported names become something other files write down, and changing them stops being a local edit.
- Keep the exported set small, return a named shape rather than a positional list, and make failure explicit.
- Move code out when a second script needs it — and not before. A single file you can read at once beats a split you do not need.
- Copies that have already diverged make the module a bug fix rather than a refactor.
Chapter 12.2 takes the mechanism that makes any of this possible: package, which gives a group of names a prefix so two files can both define a parse_log without colliding — and which, as the measurement there shows, separates names without limiting how long anything lives.
Where this fits
Part of the Perl curriculum.
