Skip to content
VLSI Mentor

Perl Part I · Module 10 · Subroutines and Arguments

Returning Structures Safely

A subroutine that wants to hand back two lists cannot simply return two lists — they arrive as one run of elements with nothing marking where the first ended, which is the same flattening that makes two arrays unpassable on the way in. References are the fix, and they are the reason Module 7 exists. This chapter is also the right place to settle a much smaller-looking question with much larger consequences: how a subroutine returns nothing. There are two ways to write it, they are identical in the context most people test, and in the other context one of them turns a failure into a result that tests as true.

Intermediate15 min readPerlPart ISubroutinesReferences

Part I · Module 10 · Chapter 10.8 · Returning Structures Safely

1. Two Lists Arrive as One

Azvya Education Pvt. Ltd.VLSI Mentor
two-arrays.pl
use strict;
use warnings;

sub as_lists {
    my @failing = ("alu", "regf");
    my @waived  = ("pad");
    return (@failing, @waived);
}

sub as_refs {
    my @failing = ("alu", "regf");
    my @waived  = ("pad");
    return (\@failing, \@waived);
}

my @flat = as_lists();
printf "returned two arrays : %d flat item(s) [%s]\n", scalar(@flat), join(" ", @flat);

my ($f, $w) = as_refs();
printf "returned two refs   : %d failing [%s], %d waived [%s]\n",
    scalar(@$f), join(" ", @$f), scalar(@$w), join(" ", @$w);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
returned two arrays : 3 flat item(s) [alu regf pad]
returned two refs   : 2 failing [alu regf], 1 waived [pad]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

Two arrays of two and one came back as a single run of three. There is no marker between regf and pad, and no way for the caller to recover the split — exit 0, nothing on STDERR.

That is Chapter 5.12's rule: lists do not nest. Chapter 10.2 §1 measured the same thing on the way in, where count(@a, @a) gave 4. A subroutine boundary flattens in both directions, and for the same reason.

The fix is the one Module 7 is built around: return a reference per container. \@failing is one scalar, \@waived is another, and two scalars in a returned list stay two — Chapter 7.1 §2's measurement, now on the way out.

ReturningCaller receives
return (@a, @b);one flat list; the boundary is lost
return (\@a, \@b);two scalars, each a reference
return [@a], [@b];two scalars, each a reference to a copy
return { failing => \@a, waived => \@b };one scalar, self-describing

The third row uses Chapter 7.5 §2's constructor, which copies rather than aliasing — relevant because of §3's lifetime question.

2. Returning One Thing, Not Several

The last row of that table is usually the best answer, and it is worth arguing for explicitly.

Azvya Education Pvt. Ltd.VLSI Mentor
return-hashref.pl
use strict;
use warnings;

sub parse {
    my ($ok) = @_;
    return { ok => 0, error => "bad input" } unless $ok;
    return { ok => 1, rows  => [ "alu", "fifo" ] };
}

for my $ok (1, 0) {
    my $r = parse($ok);
    if ($r->{ok}) {
        printf "ok   : %d row(s) [%s]\n", scalar(@{ $r->{rows} }), join(" ", @{ $r->{rows} });
    }
    else {
        printf "fail : %s\n", $r->{error};
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
ok   : 2 row(s) [alu fifo]
fail : bad input

Every path returns one scalar — a hash reference. Three things follow, and together they are why this is the shape to reach for:

  • Context stops mattering. my $r = parse(...) is the only sensible call, so Chapter 10.6's array-versus-literal question cannot arise and wantarray is unnecessary.
  • The fields are named. $r->{rows} says what it is; (my $rows, my $err) = parse(...) requires the caller to remember an order.
  • It extends without breaking callers. Adding warnings => [...] to the returned hash changes nothing for existing callers. Adding a fourth element to a returned list changes every destructuring assignment.

That last point is the practical one. A subroutine returning a positional list has its arity baked into every call site:

Azvya Education Pvt. Ltd.VLSI Mentor
Returning a list — adding a field breaks callers
my ($rows, $errors)           = parse($path);     # today
my ($rows, $errors, $warnings) = parse($path);    # every caller must change
Azvya Education Pvt. Ltd.VLSI Mentor
Returning a hash reference — adding a field breaks nothing
my $r = parse($path);
# $r->{warnings} is simply available to callers that want it

Return a list only when it is genuinely one kind of thing — return @failing_blocks; is fine and clear. Return a hash reference as soon as the answer has parts.

3. The Lifetime Question

A reference to a my variable inside a subroutine outlives the subroutine, which surprises people arriving from C.

Azvya Education Pvt. Ltd.VLSI Mentor
This is correct and idiomatic
sub make_rows {
    my @rows = ("alu", "fifo");
    return \@rows;              # @rows goes out of scope here
}

my $r = make_rows();
printf "%d rows survive\n", scalar(@$r);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
2 rows survive

Chapter 7.2 §3 measured this: a reference keeps the data alive past the variable's scope. Perl frees the array only when the last reference to it goes away, so returning \@rows is safe and needs no static and no manual allocation.

Two consequences worth being deliberate about:

Each call gets a fresh container. make_rows() called twice returns two independent references, because my @rows is a new array each time. That is almost always what you want, and it is the opposite of what a static would give.

Returning a reference to something the caller passed in shares it. This is the case to watch:

Azvya Education Pvt. Ltd.VLSI Mentor
Shared, not copied
sub sorted_ref {
    my ($aref) = @_;
    @$aref = sort @$aref;        # modifies the CALLER's array
    return $aref;                # and hands back the same one
}

That subroutine sorts in place and returns an alias to the caller's own array, which may be what you intended — but a caller who writes my $sorted = sorted_ref(\@names); and then expects @names to be untouched is wrong. Chapter 7.13 is the chapter on which copies share what; the one-line summary is that return [ @$aref ] hands back a copy of the outer list and return $aref hands back the original.

4. Returning Nothing

5. Signalling Failure and Returning Data

Combining §2 and §4 gives three workable designs, and it is worth knowing which to pick.

Azvya Education Pvt. Ltd.VLSI Mentor
1. Bare return for nothing-found
sub find_block {
    my ($name) = @_;
    return unless exists $DB{$name};
    return $DB{$name};
}

my $b = find_block("alu") or warn "no such block\n";

Simplest, and right when "not found" needs no explanation. The caller tests the return value directly.

Azvya Education Pvt. Ltd.VLSI Mentor
2. A status hash reference for anything with a reason
sub parse_report {
    my ($path) = @_;
    return { ok => 0, error => "not found: $path" } unless -e $path;
    return { ok => 0, error => "empty: $path" }     unless -s $path;
    return { ok => 1, rows => \@rows };
}

This is Chapter 8.6's three-state verdict as a return value, and it is the right shape whenever there is more than one way to fail. The caller gets a reason it can log.

Azvya Education Pvt. Ltd.VLSI Mentor
3. Return data, die on failure
sub must_parse {
    my ($path) = @_;
    die "cannot read $path: $!\n" unless -r $path;
    return \@rows;
}

Right when the caller cannot sensibly continue. die plus an eval at a sensible boundary keeps the happy path free of status checks — and Chapter 3.9 covers or die and its exit status.

Pick one per subroutine and do not mix. A subroutine that sometimes returns a status hash and sometimes dies forces every caller to handle both.

What to avoid is the fourth option, which is common and bad:

Azvya Education Pvt. Ltd.VLSI Mentor
Do not: a magic value in the data's own type
return -1;          # for a function that returns a count
return "";          # for a function that returns a name
return 0;           # for a function that returns a slack

Chapter 8.6 is the full argument: a sentinel inside the valid range of the return type cannot be distinguished from a real answer. A slack of 0 is a real measurement, and -1 is a real count difference. return with nothing is outside every value type, which is exactly why it works.

6. Why This Matters in Engineering Work

Parsers return more than one thing. Rows plus the corner they came from, failing blocks plus waived ones, a count plus a worst value. Returning those as a flat list loses the boundaries silently; returning a hash reference names them and survives the next field someone adds.

The return undef bug reaches production. It is correct in scalar context, which is how most people test a helper, and wrong in list context, which is how loops call it. The symptom is a dereference error inside a guarded branch — the guard passed, so the investigation starts in the wrong place.

Returning a reference to a local is safe, and knowing that removes the instinct to return copies defensively. Each call gets a fresh container.

And one return shape per subroutine is what makes it reusable in a second script without the caller reading the body — which is Chapter 10.12's whole subject.

7. Common Mistakes

return (@a, @b); They arrive flat with no boundary. Return references.

return undef; In list context that is a one-element list, so if (@r) is true on failure — and the caller prints a blank row and exits 0 rather than crashing. Use a bare return.

A positional list return that later needs a third field. Every destructuring caller must change. Return a hash reference.

Returning different shapes from different paths. The caller cannot write one handling.

Expecting a returned reference to a my array to dangle. It does not; the data outlives the scope.

return $aref after modifying @$aref. That shares the caller's array; return [ @$aref ] copies the outer list.

A sentinel inside the return type's valid range — -1, 0, "". Use a bare return or a status hash.

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

8. Exercise

Exercise 1 — the flattening. Write a subroutine returning two arrays of two and one elements. Print the count the caller receives. Then rewrite it returning two references and print both counts.

Exercise 2 — four ways. For the same two arrays, write all four rows of §1's table and report what the caller receives for each.

Exercise 3 — one shape. Write §2's parse returning a hash reference on both paths, and a caller handling both. Then add a warnings field and confirm the existing caller still works unchanged.

Exercise 4 — the arity break. Write a subroutine returning ($rows, $errors) and a caller destructuring both. Add a third returned field without changing the caller, and record what the caller now holds.

Exercise 5 — lifetime. Return \@rows from a subroutine where @rows is a my variable. Confirm the caller sees both elements. Then call it twice and confirm the two references are different with ==.

Exercise 6 — the sharing case. Write sorted_ref from §3, call it with \@names, and check whether @names was modified. Then change it to return [ sort @$aref ] and check again.

Exercise 7 — the four cells. Write nothing and undef_ret from §4. Call each in scalar and list context and record all four results. Say which single cell differs.

Exercise 8 — the downstream failure. Write a find_rows that uses return undef for not-found, and a caller that guards with if (@rows), prints a field off each row, and then reports ref($r). Record STDOUT, STDERR and the exit status, and say what $r became. Then change the field read to @{ $r->{list} } and record how that differs. Finally switch to a bare return and confirm the guard now works.

9. Summary

  • Two returned arrays arrive as one flat list — measured, arrays of 2 and 1 came back as 3 items with no boundary, exit 0. Same rule as Chapter 10.2's inbound flattening.
  • Return a reference per container, or better, one hash reference naming the parts.
  • A hash reference makes context irrelevant, names the fields, and extends without breaking callers. A positional list return bakes its arity into every call site.
  • Return a plain list only when it is one kind of thing.
  • A reference to a my array outlives the subroutine — Chapter 7.2 §3 — and each call gets a fresh container.
  • return $aref after modifying @$aref shares the caller's array. return [ @$aref ] copies the outer list.
  • return undef gives a ONE-element list in list context, so if (@result) is TRUE on failure — measured. And the caller does not then crash: reading a field off the undef row autovivifies it into an empty hash, prints a blank row, warns once, and exits 0.
  • Write a bare return. It gives undef in scalar context and an empty list in list context — correct in both. return () is equivalent and less conventional.
  • Pick one failure design per subroutine: a bare return, a status hash reference with a reason, or die. Never a sentinel inside the return type's valid range.

Chapter 10.9 turns to subroutines that call themselves, for data whose depth you do not know in advance — and the case that stops the calling.

Where this fits

Part of the Perl curriculum.