JohnnyLoo
JohnnyLoo

Reputation: 927

Perl unexpected result

Imagine I have this Perl script

my $name = "   foo    ";
my $sn   = "     foosu";

trim($name, \$sn);

print "name: [$name]\n";
print "sn: [$sn]\n";
exit 0;


sub trim{

   my $fref_trim = sub{
                          my ($ref_input) = @_;
                          ${$ref_input} =~ s/^\s+// ;
                          ${$ref_input} =~ s/\s+$// ;
    };

   foreach my $input (@_){
       if (ref($input) eq "SCALAR"){
            $fref_trim->($input);
       } else {
            $fref_trim->(\$input);
       }
   }
}

Result:

name: [foo]
sn: [foosu]

I would expect $name to be "[ foo ]" when printing the value after calling trim, but the sub is setting $name as I would want it. Why is this working, when it really shouldn't?

I'm not passing $name by reference and the trim sub is not returning anything. I'd expect the trim sub to create a copy of the $name value, process the copy, but then the original $name would still have the leading and trailing white spaces when printed in the main code.

I assume it is because of the alias with @_, but shouldn't the foreach my $input (@_) force the sub to copy the value and only treat the value not the alias?

I know I can simplify this sub and I used it only as an example.

Upvotes: 2

Views: 170

Answers (2)

Sinan Ünür
Sinan Ünür

Reputation: 118118

Elements of @_ are aliases to the original variables. What you are observing is the difference between:

sub ltrim {
    $_[0] =~ s/^\s+//;
    return $_[0];
}

and

sub ltrim {
    my ($s) = @_;
    $s =~ s/^\s+//;
    return $s;
}

Compare your code to:

#!/usr/bin/env perl

my $name = "   foo    ";
my $sn   = "     foosu";

trim($name, \$sn);

print "name: [$name]\n";
print "sn: [$sn]\n";

sub trim {
    my @args = @_;

    my $fref_trim = sub{
        my ($ref_input) = @_;
        ${$ref_input} =~ s/^\s+//;
        ${$ref_input} =~ s/\s+\z//;
    };

   for my $input (@args) {
       if (ref($input) eq "SCALAR") {
           $fref_trim->($input);
       }
       else {
           $fref_trim->(\$input);
       }
   }
}

Output:

$ ./zz.pl
name: [   foo    ]
sn: [foosu]

Note also that the loop variable in for my $input ( @array ) does not create a new copy for each element of the array. See perldoc perlsyn:

The foreach loop iterates over a normal list value and sets the scalar variable VAR to be each element of the list in turn. ...

...

the foreach loop index variable is an implicit alias for each item in the list that you're looping over.

In your case, this would mean that, at each iteration $input is an alias to the corresponding element of @_ which itself is an alias to the variable that was passed in as an argument to the subroutine.

Making a copy of @_ thus prevents the variables in the calling context from being modified. Of course, you could do something like:

sub trim {
    my $fref_trim = sub{
        my ($ref_input) = @_;
        ${$ref_input} =~ s/^\s+//;
        ${$ref_input} =~ s/\s+\z//;
    };

   for my $input (@_) {
       my $input_copy = $input;
       if (ref($input_copy) eq "SCALAR") {
           $fref_trim->($input_copy);
       }
       else {
           $fref_trim->(\$input_copy);
       }
   }
}

but I find making a wholesale copy of @_ once to be clearer and more efficient assuming you do not want to be selective.

Upvotes: 8

melpomene
melpomene

Reputation: 85767

I assume it is because of the alias with @_, but shouldn't the foreach my $input (@_) force the sub to copy the value and only treat the value not the alias?

You're right that @_ contains aliases. The part that's missing is that foreach also aliases the loop variable to the current list element. Quoting perldoc perlsyn:

If any element of LIST is an lvalue, you can modify it by modifying VAR inside the loop. Conversely, if any element of LIST is NOT an lvalue, any attempt to modify that element will fail. In other words, the foreach loop index variable is an implicit alias for each item in the list that you're looping over.

So ultimately $input is an alias for $_[0], which is an alias for $name, which is why you see the changes appearing in $name.

Upvotes: 2

Related Questions