Search code examples
perlsubroutinescoping

can a variable be defined in perl whose value cannot be changed in a subroutine?


In the following script, I declare and modify @basearray in the main program. Inside the dosomething subroutine, I access @basearray, assign it to an array local to the script, and modify the local copy. Because I have been careful to change the value only of local variables inside the subroutine, @basearray is not changed.

If I had made the mistake of assigning a value to @basearray inside the subroutine, though, it would have been changed and that value would have persisted after the call to the subroutine.

This is demonstrated in the 2nd subroutine, doagain.

Also, doagain receives the reference \@basearray as an argument rather than accessing @basearray directly. But going to that additional trouble provides no additional safety. Why do it that way at all?

Is there a way to guarantee that I cannot inadvertently change @basearray inside any subroutine? Any kind of hard safety device that I can build into my code, analogous to use strict;, some combination perhaps of my and local?

Am I correct in thinking that the answer is No, and that the only solution is to not make careless programmer errors?

#!/usr/bin/perl
use strict; use warnings;
my @basearray = qw / amoeba /;
my $count;

{
    print "\@basearray==\n"; 
    $count = 0; 
    foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}

sub dosomething 
{
    my $sb_name = (caller(0))[3];
    print "entered $sb_name\n";
    my @sb_array=( @basearray , 'dog' );
    {
        print "\@sb_array==\n"; 
        $count = 0; 
        foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
    }
    print "return from $sb_name\n";
}

dosomething();
@basearray = ( @basearray, 'rats' );

{
    print "\@basearray==\n"; 
    $count = 0; 
    foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}


sub doagain
{
    my $sb_name = (caller(0))[3];
    print "entered $sb_name\n";
    my $sf_array=$_[0];
    my @sb_array=@$sf_array;
    @sb_array=( @sb_array, "piglets ... influenza" );
    {
        print "\@sb_array==\n"; 
        $count = 0; 
        foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
    }
    print "now we demonstrate that passing an array as an argument to a subroutine does not protect it from being globally changed by programmer error\n";
    @basearray = ( @sb_array );
    print "return from $sb_name\n";
}
doagain( \@basearray );

{
    print "\@basearray==\n"; 
    $count = 0; 
    foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}


Solution

  • (Putting my comment as answer) One way to guarantee not changing a variable inside a subroutine is to not change it. Use only lexically scoped variables inside the subroutine, and pass whatever values you need inside the subroutine as arguments to the subroutine. It is a common enough coding practice, encapsulation.

    One idea that you can use -- mainly as practice, I would say -- to force yourself to use encapsulation, is to put a block around your "main" code, and place subroutines outside of it. That way, if you should accidentally refer to a (formerly) global variable, use strict will be able to do it's job and produce a fatal error. Before runtime.

    use strict;
    use warnings;
    
    main: {                      # lexical scope reduced to this block
        my @basearray = qw / amoeba /;
        print foo(@basearray);   # works
        print bar();             # fatal error
    } # END OF MAIN                lexical scope of @basearray ends here
    
    sub foo {
        my @basearray = @_;      # encapsulated 
        return $basearray[1]++;
    }
    sub bar {
        return $basearray[1]++;  # out of scope ERROR
    }
    

    This will not compile, and will produce the error:

    Global symbol "@basearray" requires explicit package name at foo.pl line 15.
    Execution of foo.pl aborted due to compilation errors.
    

    I would consider this a training device to force yourself to using good coding practices, and not something to necessarily use in production code.