I'm trying to figure out Perl subroutines and how they work.
From perlsub I understand that subroutines are call-by-reference and that an assignment (like my(@copy) = @_;
) is needed to turn them into call-by-value.
In the following, I see that change
is called-by-reference because "a" and "b" are changed into "x" and "y". But I'm confused about why the array isn't extended with an extra element "z"?
use strict;
use Data::Dumper;
my @a = ( "a" ,"b" );
change(@a);
print Dumper(\@a);
sub change
{
@_[0] = "x";
@_[1] = "y";
@_[2] = "z";
}
Output:
$VAR1 = [
'x',
'y'
];
In the following, I pass a hash instead of an array. Why isn't the key changed from "a" to "x"?
use strict;
use Data::Dumper;
my %a = ( "a" => "b" );
change(%a);
print Dumper(\%a);
sub change
{
@_[0] = "x";
@_[1] = "y";
}
Output:
$VAR1 = {
'a' => 'y'
};
I know the real solution is to pass the array or hash by reference using \@
, but I'd like to understand the behaviour of these programs exactly.
A Perl subroutine or function is a group of statements that together performs a task. You can divide up your code into separate subroutines. How you divide up your code among different subroutines is up to you, but logically the division usually is so each function performs a specific task.
Passing parameters by references As mentioned in the previous Perl subroutine tutorial, when you change the values of the elements in the argument arrays @_, the values of the corresponding arguments change as well. This is known as the passing parameter by reference.
The word subroutines is used most in Perl programming because it is created using keyword sub.
Perl always passes by reference. It's just that sometimes the caller passes temporary scalars.
The first thing you have to realise is that the arguments of subs can be one and only one thing: a list of scalars.* One cannot pass arrays or hashes to them. Arrays and hashes are evaluated, returning a list of their content. That means that
f(@a)
is the same** as
f($a[0], $a[1], $a[2])
Perl passes by reference. Specifically, Perl aliases each of the arguments to the elements of @_
. Modifying the elements @_
will change the scalars returned by $a[0]
, etc. and thus will modify the elements of @a
.
The second thing of importance is that the key of an array or hash element determines where the element is stored in the structure. Otherwise, $a[4]
and $h{k}
would require looking at each element of the array or hash to find the desired value. This means that the keys aren't modifiable. Moving a value requires creating a new element with the new key and deleting the element at the old key.
As such, whenever you get the keys of an array or hash, you get a copy of the keys. Fresh scalars, so to speak.
Back to the question,
f(%h)
is the same** as
f(
my $k1 = "a", $h{a},
my $k2 = "b", $h{b},
my $k2 = "c", $h{c},
)
@_
is still aliased to the values returned by %h
, but some of those are just temporary scalars used to hold a key. Changing those will have no lasting effect.
* — Some built-ins (e.g. grep
) are more like flow control statements (e.g. while
). They have their own parsing rules, and thus aren't limited to the conventional model of a sub.
** — Prototypes can affect how the argument list is evaluated, but it will still result in a list of scalars.
Perl's subroutines accept parameters as flat lists of scalars. An array passed as a parameter is for all practical purposes a flat list too. Even a hash is treated as a flat list of one key followed by one value, followed by one key, etc.
A flat list is not passed as a reference unless you do so explicitly. The fact that modifying $_[0]
modifies $a[0]
is because the elements of @_
become aliases for the elements passed as parameters. Modifying $_[0]
is the same as modifying $a[0]
in your example. But while this is approximately similar to the common notion of "pass by reference" as it applies to any programming language, this isn't specifically passing a Perl reference; Perl's references are different (and indeed "reference" is an overloaded term). An alias (in Perl) is a synonym for something, where as a reference is similar to a pointer to something.
As perlsyn states, if you assign to @_
as a whole, you break its alias status. Also note, if you try to modify $_[0]
, and $_[0]
happens to be a literal instead of a variable, you'll get an error. On the other hand, modifying $_[0]
does modify the caller's value if it is modifiable. So in example one, changing $_[0]
and $_[1]
propagates back to @a
because each element of @_
is an alias for each element in @a
.
Your second example is a little tricky. Hash keys are immutable. Perl doesn't provide a way to modify a hash key, aside from deleting it. That means that $_[0]
is not modifiable. When you attempt to modify $_[0]
Perl cannot comply with that request. It probably ought to throw a warning, but doesn't. You see, the flat list passed to it consists of unmodifiable-key followed by modifiable-value, etc. This is mostly a non-issue. I cannot think of any reason to modify individual elements of a hash in the way you're demonstrating; since hashes have no particular order you wouldn't have simple control over which elements in @_
propagate back to which values in %a
.
As you pointed out, the proper protocol is to pass \@a
or \%a
, so that they can be referred to as $_[0]->{element}
or $_[0]->[0]
. Even though the notation is a little more complicated, it becomes second nature after awhile, and is much clearer (in my opinion) as to what is going on.
Be sure to have a look at the perlsub documentation. In particular:
Any arguments passed in show up in the array
@_
. Therefore, if you called a function with two arguments, those would be stored in$_[0]
and$_[1]
. The array@_
is a local array, but its elements are aliases for the actual scalar parameters. In particular, if an element$_[0]
is updated, the corresponding argument is updated (or an error occurs if it is not updatable). If an argument is an array or hash element which did not exist when the function was called, that element is created only when (and if) it is modified or a reference to it is taken. (Some earlier versions of Perl created the element whether or not the element was assigned to.) Assigning to the whole array@_
removes that aliasing, and does not update any arguments.
(Note that use warnings
is even more important than use strict
.)
@_
itself isn't a reference to anything, it is an array (really, just a view of the stack, though if you do something like take a reference to it, it morphs into a real array) whose elements each are an alias to a passed parameter. And those passed parameters are the individual scalars passed; there is no concept of passing an array or hash (though you can pass a reference to one).
So shifts, splices, additional elements added, etc. to @_
don't affect anything passed, though they may change the index of or remove from the array one of the original aliases.
So where you call change(@a)
, this puts two aliases on the stack, one to $a[0]
and one to $a[1]
. change(%a)
is more complicated; %a
flattens out into an alternating list of keys and values, where the values are the actual hash values and modifying them modifies what's stored in the hash, but where the keys are merely copies, no longer associated with the hash.
Perl does not pass the array or hash itself by reference, it unfurls the entries (the array elements, or the hash keys and values) into a list and passes this list to the function. @_ then allows you to access the scalars as references.
This is roughly the same as writing:
@a = (1, 2, 3);
$b = \$a[2];
${$b} = 4;
@a now [1, 2, 4];
You'll note that in the first case you were not able to add an extra item to @a, all that happened was that you modified the members of @a that already existed. In the second case, the hash keys don't really exist in the hash as scalars, so these need to be created as copies in temporary scalars when the expanded list of the hash is created to be passed into the function. Modifying this temporary scalar will not modify the hash key, as it is not the hash key.
If you want to modify an array or hash in a function, you will need to pass a reference to the container:
change(\%foo);
sub change {
$_[0]->{a} = 1;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With