Reputation: 1306
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" };
}
Upvotes: 2
Views: 595
Reputation: 1306
Here is a prototype that uses @TLP's answer.
#!/usr/bin/perl
use strict; use warnings;
{ # block_main BEG
my @basearray = qw / amoeba elephants sequoia /;
print join ( ' ', 'in main, @basearray==', join ( ' ', @basearray ), "\n" );
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
my $ref_basearray = changearray ( \@basearray, 'wolves or coyotes . . . ' );
@basearray = @$ref_basearray;
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
} # block_main END
sub enumerateprintarray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # mortal sin! for in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 1;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my $sf_array = $_[0];
my @sb_array = @$sf_array;
my $sb_count = 0;
foreach (@sb_array)
{
$sb_count++;
print "\t$sb_count:\t$_\n";
}
}
sub changearray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 2;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my ( $sf_array, $addstring ) = @_;
my @sb_array = @$sf_array;
push @sb_array, $addstring;
return ( \@sb_array );
}
Upvotes: 0
Reputation: 13664
TLDR: yes, but.
I'll start with the "but". But it's better to design your code so that the variable simply doesn't exist in the scope where the untrusted function is defined.
sub untrusted_function {
...
}
my @basearray = qw( ... ); # declared after untrusted_function
If untrusted_function
needs to be able to access the contents of the array, pass it a copy of the array as a parameter, so it can't modify the original.
Now here's the "yes".
You can mark the array as read-only before calling the untrusted function.
Internals::SvREADONLY($_, 1) for @basearray;
Internals::SvREADONLY(@basearray, 1);
Then mark it read-write again after the function has finished.
Internals::SvREADONLY(@basearray, 0);
Internals::SvREADONLY($_, 0) for @basearray;
Using Internals::SvREADONLY(@basearray, $bool)
modifies the read-only state of the array itself, preventing elements from being added or removed from it; Internals::SvREADONLY($_, $bool) for @basearray
modifies the read-only state of each element in the array too, which you probably want.
Of course, if your array contains references like blessed objects, you then need to consider whether you need to recurse into the references, marking them read-only too. (But can also be a concern with the shallow copy of the array I mentioned in the preferred solution!)
So yes, it is possible to prevent a sub from accidentally modifying a variable by marking that variable read-only before calling the sub, but it's a better idea to restructure your code so the sub simply doesn't have access to the variable at all.
Yes, but.
Upvotes: 1
Reputation: 67900
(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.
Upvotes: 2
Reputation: 66883
There isn't a pragma or a keyword or such, but there are well established "good practices," which in this case completely resolve what you reasonably ponder about.
The first sub, dosomething
, commits the sin of using variables visible in its scope but defined in the higher scope. Instead, always pass needed data to a subroutine (exceptions are rare, in crystal clear cases).
Directly using data from "outside" defies the idea of a function as an encapsulated procedure, exchanging data with its users via a well defined and clear interface. It entangles ("couples") sections of code that are in principle completely unrelated. In practice, it can also be outright dangerous.
Also, the fact the @basearray
is up for grabs in the sub is best considered an accident -- what when that sub gets moved to a module? Or another sub is introduced to consolidate code where @basearray
is defined?
The second sub, doagain
, nicely takes a reference to that array. Then, to protect the data in the caller, one can copy the caller's array to another one which is local to the sub
sub doagain {
my ($ref_basearray) = @_;
my @local_ba = @$ref_basearray;
# work with local_ba and the caller's basearray is safe
}
The names of local lexical variables are of course arbitrary, but a convention where they resemble the caller's data names may be useful.
Then you can adopt a general practice, for safety, to always copy input variables to local ones. Work directly with references that are passed in only when you want to change the caller's data (relatively rare in Perl). This may hurt efficiency if it's done a lot with sizeable data, or when really large data structures are involved. So perhaps then make an exception and change data via its reference, and be extra careful.
Upvotes: 4
Reputation: 118605
There are several solutions with various levels of pithiness from "just don't change it" to "use an object or tied array and lock down the update functions". An intermediate solution, not unlike using an object with a getter method, is to define a function that returns your array but can only operate as an rvalue, and to use that function inside subroutines.
my @basearray = (...);
sub basearray { return @basearray }
sub foo {
foreach my $elem (basearray()) {
...
}
@bar = map { $_ *= 2 } basearray(); # ok
@bar = map { $_ *= 2 } @basearray; # modifies @basearray!
}
Upvotes: 2