Arguments and @_#

When a sub is called the classic way (no signature), Perl flattens the entire argument list into a single array and exposes it inside the body as @_. The elements of that array are not copies — they are aliases to the caller’s expressions. This single rule is the source of most of Perl’s calling-convention surprises and most of its calling-convention power.

The aliasing rule#

sub bump {
    $_[0]++;                     # mutates the caller's variable
}

my $x = 10;
bump($x);
say $x;                          # 11

Modifying $_[0] writes through the alias to whatever the caller passed. That is by design — it is how chomp(@lines) manages to strip newlines from every element of @lines rather than from a copy of @lines.

The aliasing extends to literals, with predictable consequences:

bump(10);                        # error: Modification of a read-only
                                 # value attempted

A literal 10 is a constant. The alias points at it; the mutation attempt dies.

The unpacking idiom#

Because the aliasing is rarely what the body wants, almost every classic-style sub starts with one of these two patterns:

# Named arguments (most common)
sub greet {
    my ($name, $greeting) = @_;  # COPY into private lexicals
    $greeting //= 'Hello';
    return "$greeting, $name!";
}

# Method dispatch
sub method_call {
    my $self = shift;            # COPY first arg, leave rest in @_
    my %args = @_;               # remaining args as a hash
    ...
}

Once unpacked, the body works with private copies and cannot accidentally mutate the caller’s state. shift with no argument inside a sub defaults to @_, which is why my $self = shift; is everywhere in OO Perl.

Legitimate uses of aliasing#

Aliasing is the right tool when you actually want to mutate caller state:

# in-place trim
sub trim_inplace {
    for (@_) {
        s/^\s+//;
        s/\s+$//;
    }
}

my @data = ("  hello  ", "  world\n");
trim_inplace(@data);
# @data is now ("hello", "world")

The for (@_) loop aliases $_ to each element of @_, which is itself an alias to each element of @data. The s/// modifications go through both alias hops to the original strings. Without the aliasing, this same shape of code would be twice as much work.

The same mechanism powers chomp(@lines), chop(@buf), and any ”work over a list and modify in place“ function you write yourself. State the intent in the function’s name (_inplace, mutate_, similar) so callers are not surprised.

Named arguments#

Perl has no built-in named-parameter syntax in the classic form, but the => (fat comma) plus a hash assignment gives the same effect:

sub configure {
    my %opt = @_;                # ('host' => 'localhost', 'port' => 8080)
    $opt{host} //= 'localhost';
    $opt{port} //= 80;
    ...
}

configure( host => 'example.com', port => 8080 );

The fat comma is identical to a regular comma in semantics but quotes the bareword on its left, so host => ... is shorthand for 'host' => .... See , for the operator itself.

For mandatory-plus-optional named args, the standard shape is:

sub render {
    my ($template, %opt) = @_;   # one positional, rest as named
    $opt{escape} //= 1;
    ...
}

render('greeting.tt', escape => 0, locale => 'de');

For the modern named-parameter idiom built on signatures, see signatures.

shift, pop, unshift, push on @_#

@_ is a real array. You can shift from the front, pop from the back, slice it, modify it. None of these change the caller’s state — only writing through $_[N] does.

sub describe {
    my $what = shift;            # remove first arg from @_
    my @rest = @_;               # copy what's left
    ...
}

sub method {
    my $self = shift;
    my @args = @_;
    $self->dispatch(@args);
}

A handy consequence: if you want to re-pack the remaining args to forward to another sub, just write @_ — that is exactly what goto &other does without the explicit pass. See recursion for tail-call forwarding.

wantarray and the calling context#

Inside a sub, wantarray tells you the context the call was made in:

wantarray

Context

Common shape

1 (true)

list

return a list

0 (false)

scalar

return a single scalar

undef

void

return early; the result will be discarded

sub items {
    return unless defined wantarray;     # void: nothing to compute
    my @result = compute_items();
    return wantarray ? @result : scalar @result;
}

@_ says what came in; wantarray says what is expected back. They are the two halves of the calling-convention picture. See lvalue and context for the full discussion.

Common pitfalls#

  • my ($x) = @_ vs my $x = @_. The first is list context: $x gets the first argument. The second is scalar context: $x gets the count of arguments. The parentheses on the left are the difference; mistakes here are common.

    sub one_arg {
        my ($x) = @_;            # $x = first arg
    }
    
    sub count_args {
        my $n = @_;              # $n = number of args
    }
    
  • Modifying $_[N] ”by accident“. Functions like chomp, chop, tr///, s/// operate on their argument in place. chomp($_[0]) mutates the caller’s string. If that’s not what you want, copy first.

  • Flattening of arrays and hashes. Arrays and hashes passed as arguments lose their identity at the call site — they all become elements of the single flat @_. To pass an array without flattening, pass a reference:

    takes_array(\@arr);          # one ref argument
    sub takes_array {
        my ($aref) = @_;
        push @$aref, 'extra';
    }
    
  • @_ after goto &sub. The current @_ is passed through to the target. Anything you’ve already shifted off is gone from the perspective of the target. This is a feature, not a bug — see recursion.

See also#

  • @_ — the variable’s perlvar entry, with the aliasing rule and the loop-context discussion.

  • shift, pop — default to @_ inside a sub.

  • Signatures — the modern alternative for named-and-positional parameter handling.

  • Return values — what the body sends back.

  • Comma operator=> for named-arg construction.