Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
the-duplication.pl
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");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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     : NO

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
lib/VLSI/Log.pm
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:

Azvya Education Pvt. Ltd.VLSI Mentor
one-source.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 it

Both 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

Two reporting scripts both calling one module, and the module's exported names shown as the interface that the move creates and that callers then depend on.report_a.plneeds the parse rulereport_b.plneeds the same ruleVLSI::Logone definitionits exported nameswhat callers rely on12
Moving a rule into a module gives both callers a single definition — and simultaneously creates something new: a set of names outside the module that other files now depend on. The second arrow is the obligation. Before the move, renaming the subroutine was a local edit in one file; afterwards it is a change to an interface, and every caller is a file you may not own.

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.12 and u_fifo/B -0.95 as 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.