Skip to content
VLSI Mentor

Perl Part I · Module 10 · Subroutines and Arguments

Designing a Subroutine Worth Reusing

Everything in this module has been a mechanism: how arguments arrive, how they alias, how a return value changes shape with context, how to keep private state. This chapter is about using those mechanisms deliberately. A subroutine that works in the script it was written for is easy; one that can be lifted into a second script without being rewritten requires three decisions, and the useful thing about them is that all three are made before the body is written. They are what goes in, what single shape comes out, and what happens when the input is not what you assumed. The same piece of work written with and without those decisions is measured here side by side, and the undesigned version fails in three ways that each trace back to an earlier chapter.

Intermediate16 min readPerlPart ISubroutines

Part I · Module 10 · Chapter 10.12 · Designing a Subroutine Worth Reusing

1. The Three Decisions

The three decisions that make a subroutine liftable into another script

Diagram showing three design decisions — what goes in, what comes out, and what happens on bad input — diverging from one subroutine and converging on reusabilityOne subroutinebefore the bodyWhat goes in?one array referenceWhat comes out?one hash referenceWhat on bad input?die with a reasonReusableno caller changes12
All three are made before the body is written, and each has a default that is almost always right: one reference in rather than a flat list, one hash reference out rather than a positional list, and die with a reason rather than returning a plausible value. A subroutine that answers all three can be moved to a second script without its callers changing.

Each decision has a default that is right most of the time, and each default is the conclusion of an earlier chapter rather than a preference:

DecisionDefaultBecause
what goes inone reference per containerChapter 10.2 §1 — @_ flattens
named arguments past threeChapter 10.5 §6
unpack on the first lineChapter 10.4 §2 — @_ aliases
what comes outone hash referenceChapter 10.8 §2
never two containersChapter 10.8 §1 — they merge
bare return for nothingChapter 10.8 §4
on bad inputdie with a reasonChapter 8.6 §2
or a third state, never a plausible valueChapter 8.6 §6

2. The Undesigned Version

Azvya Education Pvt. Ltd.VLSI Mentor
design-bad.pl
use strict;
use warnings;

# three decisions left unmade
sub summarise {
    my @rows = @_;
    my $n = 0;
    my $worst;
    for my $r (@rows) {
        next unless $r->{slack} < 0;
        $n++;
        $worst = $r->{slack} if !defined $worst || $r->{slack} < $worst;
    }
    print "failing: $n, worst: $worst\n";       # prints instead of returning
    return ($n, $worst);                        # and also returns a list
}

my @rows = (
    { blk => "alu",  slack => -0.12 },
    { blk => "fifo", slack =>  0.34 },
);
my ($count) = summarise(@rows);
printf "caller got count=%s\n", $count;

my $what = summarise(@rows);
printf "scalar context gave %s\n", $what;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
failing: 1, worst: -0.12
caller got count=1
failing: 1, worst: -0.12
scalar context gave -0.12
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

It computes the right answer, and it has three defects — each of which traces to a chapter in this module.

1. It prints. Lines 1 and 3 of that output came from inside the subroutine. A caller who wants the numbers in a report, or wants to suppress them, or calls it twice — as this one does — cannot. Printing is a side effect the caller did not ask for and cannot turn off, and it appears twice because the subroutine was called twice.

2. It returns a positional list. my ($count) = summarise(@rows) works today. Add a third figure — the count of unknown rows — and every destructuring caller has to change, which Chapter 10.8 §2 measured as the cost of a positional return.

3. In scalar context it hands back the worst slack. my $what = summarise(@rows) gave -0.12, not 1. A caller writing my $n = summarise(@rows) meaning "how many failed" gets a slack value — because Chapter 10.6 §1 established that a returned list literal in scalar context gives its last element. Nothing warns.

There is a fourth problem the measurement does not show, because the input was clean: $r->{slack} < 0 on a row with no slack key compares undef, and on a row whose slack is "N/A" compares a string — Chapter 8.1 §5 measured that both numify to 0 and are therefore classified as passing.

3. The Designed Version

Azvya Education Pvt. Ltd.VLSI Mentor
design-good.pl
use strict;
use warnings;

# inputs: one array reference. returns: one hash reference. bad input: dies.
sub summarise {
    my ($rows) = @_;

    die "summarise: expected an array reference\n"
        unless ref $rows eq "ARRAY";

    my %out = (checked => 0, failing => 0, worst => undef, unknown => 0);

    for my $r (@$rows) {
        unless (ref $r eq "HASH" && defined $r->{slack}
                && $r->{slack} =~ /^[-+]?\d+(?:\.\d+)?$/) {
            $out{unknown}++;
            next;
        }
        $out{checked}++;
        next unless $r->{slack} < 0;
        $out{failing}++;
        $out{worst} = $r->{slack}
            if !defined $out{worst} || $r->{slack} < $out{worst};
    }

    return \%out;
}

my @rows = (
    { blk => "alu",  slack => -0.12 },
    { blk => "fifo", slack =>  0.34 },
    { blk => "regf", slack => "N/A" },
    { blk => "pad" },
);

my $s = summarise(\@rows);
printf "checked=%d failing=%d unknown=%d worst=%s\n",
    $s->{checked}, $s->{failing}, $s->{unknown},
    (defined $s->{worst} ? sprintf("%+.2f", $s->{worst}) : "none");

my $empty = summarise([]);
printf "empty input: checked=%d failing=%d worst=%s\n",
    $empty->{checked}, $empty->{failing},
    (defined $empty->{worst} ? $empty->{worst} : "none");

my $r = eval { summarise("not a ref") };
print "bad input: ", ($r ? "accepted\n" : "rejected: $@");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
checked=2 failing=1 unknown=2 worst=-0.12
empty input: checked=0 failing=0 worst=none
bad input: rejected: summarise: expected an array reference

Same arithmetic, and now:

  • Nothing is printed by the subroutine. The caller formats, and formats differently for the two calls.
  • One hash reference comes back, so context cannot matter — my $s = summarise(...) is the only sensible call. Adding a field breaks nobody.
  • The two unusable rows are counted separately as unknown, rather than being silently classified as passing. That is Chapter 8.6's three-state verdict as a return field.
  • The empty input degrades: zeros and worst => undef, which the caller renders as none rather than a fabricated +0.00 — Chapter 9.9 §3's finding.
  • A wrong-shaped argument dies with a message naming the subroutine.

4. Decision One: What Goes In

Azvya Education Pvt. Ltd.VLSI Mentor
The defaults, in order of preference
sub f { my ($rows) = @_;            ... }   # one reference
sub f { my ($rows, $opts) = @_;     ... }   # two references
sub f { my ($rows, %opt)  = @_;     ... }   # one reference + named options
sub f { my @values = @_;            ... }   # a variadic list of one kind

Pass containers as references, because Chapter 10.2 §1 measured that two arrays arrive as one flat run with no boundary. The only case for a bare list is a genuinely variadic argument — sum, max, longest — where position means nothing.

Unpack on the first line and never mention @_ again. Chapter 10.3 §1 established the readability reason and Chapter 10.4 §2 the stronger one: @_ aliases the caller's variables, so a later edit that writes to $_[0] reaches outside.

Switch to named arguments at three parameters, or at two optional ones — Chapter 10.5 §6 — and put the defaults first in the merge, which §2 there measured as the thing that silently ignores the caller when reversed.

Decide whether you modify what you were given, and say so in the name. A subroutine that sorts its argument in place is sort_in_place(\@rows); one that does not is sorted(\@rows) returning a new reference. Chapter 10.4 §4 and Chapter 7.13 cover which copies share what.

5. Decision Two: What Comes Out

6. Decision Three: What Happens on Bad Input

Three designs, and the choice is about who can do something about the problem.

Azvya Education Pvt. Ltd.VLSI Mentor
1. die — the caller cannot continue
die "summarise: expected an array reference\n" unless ref $rows eq "ARRAY";

For a programming error — the wrong kind of argument, a missing required parameter. The caller passed something impossible, so there is no sensible value to return and no recovery. Die with the subroutine's name in the message, because that is what tells whoever reads the log which contract was broken.

Azvya Education Pvt. Ltd.VLSI Mentor
2. a third state — the data is bad, not the call
$out{unknown}++ unless usable($r);

For unusable data, which is different. The call was correct; a row in the input was not. Chapter 8.6 measured what happens when a check has no third state: a two-state verdict reported PASS for four of six unusable inputs and exited 0. Counting them separately is what makes the summary honest — checked=2 unknown=2 says something failing=1 alone does not.

Azvya Education Pvt. Ltd.VLSI Mentor
3. a bare return — nothing found, and that is ordinary
return unless exists $DB{$name};

For a lookup that legitimately misses. Not an error, not bad data, just absence — and the caller tests the return value.

Pick one per subroutine. A subroutine that sometimes dies and sometimes returns a status hash forces every caller to handle both, which is the thing a contract is supposed to prevent.

One more thing worth deciding explicitly: what the subroutine does with an empty input. Chapter 9.9 §3 measured that counts and lists degrade gracefully to zero and empty, while a minimum is undef and a mean is Illegal division by zero, fatal. §3's version returns worst => undef and leaves the rendering to the caller, which reports none. Returning 0 there would have been a number that was never measured.

7. A Checklist

Before writing the body:

  • Inputs — one reference per container? Named arguments if more than three? Unpacked on the first line? Does it modify what it was given, and does the name say so?
  • Returns — one shape from every path? A hash reference if there are parts? A bare return for nothing? No sentinel inside the value range?
  • Bad input — die for a broken call, a third state for bad data, a bare return for an honest miss? One of the three, not two?
  • Empty input — what does each returned figure become? Is any of them a fabricated number?
  • Name — does it say what comes back, and whether anything is modified?

Two notes on the last point, because naming carries more of the contract than it looks:

Azvya Education Pvt. Ltd.VLSI Mentor
Names that answer a question
failing_blocks()      # returns a list — plural says so
failing_count()       # returns a number
sorted(\@rows)        # returns a new thing
sort_in_place(\@rows) # modifies the argument
must_parse($path)     # dies on failure
try_parse($path)      # returns a status

A reader who sees failing_count() will not assign it to an array, and one who sees sort_in_place will not expect the original to survive. That is contract documentation costing no lines at all.

8. Module 10, Closed

Perl's subroutines have almost no declaration — sub, a name, a block — and the consequence runs through the whole module: the language makes very few decisions for you, so the ones you do not make explicitly are made by defaults that are rarely what you wanted.

Arguments arrive in one flat array with no names and no count check, so a wrong call is not an error — Chapter 10.2 measured an extra argument being silently discarded. They arrive as aliases, so a subroutine can write to its caller's variables with nothing visible at the call site — Chapter 10.4. And what a return means is decided by the caller afterwards, so the same statement hands back a last element or a count depending on a line in a different file — Chapter 10.6.

The traps in this module share the property that made Modules 8 and 9's worth collecting — none of them produces an error:

  • An extra argument is discarded in silence, and the result is identical to a correct call.
  • $_[0] = ... changes the caller's variable, with nothing at the call site to show it.
  • ||= for a default destroys a deliberate 0 — and 0 is what --retries 0 passes.
  • my %args = (@_, DEFAULTS) ignores the caller entirely, and a no-argument test passes.
  • return (10,20,30) in scalar context gives 30; return @a gives 3 — and the two are identical in list context, so list-context tests cannot tell them apart.
  • return undef makes if (@result) true on failure, and the caller then prints a blank row and exits 0.
  • Two returned containers arrive merged, with the boundary gone.

Two are worse than silent: a misspelled argument name in a named call is ignored entirely (Chapter 10.5 §5), and an undefined subroutine is a runtime error that perl -c reports as syntax OK (Chapter 10.1 §5).

Against all of that, the module's practical advice is four lines:

Unpack on the first line. Default with //=. Return one shape. Decide what bad input does.

Each of those closes a whole class, and none costs more than a line. The rest — wantarray, prototypes, redo-style cleverness — is worth being able to read and rarely worth writing.

Module 11 turns to regular expressions, which is the largest module in Part I at twenty-two chapters, and the one where Perl's defaults are most worth understanding before relying on them.

9. Common Mistakes

Printing from inside a subroutine that computes a value. The caller cannot format, suppress or reuse it.

Returning a positional list that might grow a field. Every destructuring caller changes. Use a hash reference.

Different return shapes on different paths. The caller cannot write one handling.

Passing containers as bare lists. They merge; pass references.

Mentioning @_ after the first line. It aliases the caller's variables.

||= for a default. A deliberate 0 becomes the default.

Defaults last in a named-argument merge. The caller is silently ignored.

Mixing die and status returns in one subroutine. Pick one.

Returning 0 for a minimum over an empty list. That is a number nobody measured; return undef.

A name that does not say what comes back. rows() against row_count() is free documentation.

10. Exercise

Exercise 1 — the undesigned version. Write §2's summarise and both callers. Record all four output lines. Then name which earlier chapter explains each of the three defects.

Exercise 2 — the scalar-context defect. From Exercise 1, explain in one sentence why my $what = summarise(@rows) gave a slack value. Then change the subroutine to return @list; instead of a literal and record what scalar context now gives.

Exercise 3 — bad rows. Add { blk => "regf", slack => "N/A" } and { blk => "pad" } to Exercise 1's input, unchanged otherwise. Record the failing count and say how the two unusable rows were classified.

Exercise 4 — the designed version. Write §3's summarise. Call it with the four-row input, with an empty array reference, and with a plain string. Record all three results.

Exercise 5 — extend it. Add a corners field to the returned hash counting distinct corners. Confirm the existing caller still works without modification.

Exercise 6 — the empty case. For the empty-input call, print every returned field. Say which one is undef and what the caller should render for it. Then try to compute a mean from the returned fields and record what happens.

Exercise 7 — the three bad-input designs. Write three versions of a find_block helper: one that dies, one that returns a status hash reference, and one with a bare return. For each, write the caller. Say which you would choose for a lookup that legitimately misses.

Exercise 8 — names. For each of §7's six example names, write the one-line subroutine it implies. Then rename summarise from §3 to say what it returns.

Exercise 9 — the checklist. Take any subroutine you have written in this module's exercises and answer all five checklist items for it. Change whatever the answers say to change.

11. Summary

  • Three decisions, made before the body: what goes in, what single shape comes out, what happens on bad input.
  • Inputs: one reference per container, named arguments past three, unpacked on the first line, and a name that says whether the argument is modified.
  • Returns: one shape from every path, a hash reference when there are parts, a bare return for nothing, and never a sentinel inside the value's own range.
  • Bad input: die for a broken call, a third state for unusable data, a bare return for an honest miss — one of the three, not two.
  • Also decide the empty case. Counts degrade to zero; a minimum is undef and a mean is fatal.
  • Measured, the undesigned version failed three ways: it printed from inside, it returned a positional list, and in scalar context it handed back -0.12 where the caller wanted a count — all with empty STDERR and exit 0.
  • The designed version returned one hash reference, counted the two unusable rows as unknown, degraded on empty input, and rejected a wrong-shaped argument by name.
  • Names carry contract: failing_blocks against failing_count, sorted against sort_in_place, must_parse against try_parse.
  • The module in four lines: unpack on the first line, default with //=, return one shape, decide what bad input does.

Where this fits

Part of the Perl curriculum.