Solved

Modification of a read-only value attempted...

Posted on 2007-11-15
10
610 Views
Last Modified: 2012-06-21
Hi Experts --

I've been looking at this too long -- I can't spot the (probably obvious) problem.  I'm hoping a fresh set of eyes will help.

Here is a stripped down version of a larger script.  The actual script has many more entries in the %species hash, does some stuff between the "foreach" loops, actually does some work on the downloaded files, etc., but this is sufficient to trigger the error:

"Modification of a read-only value attempted at C:\Documents and Settings\Mike\Desktop\bf gene data\test.pl line 31."

The FTP portion retrieves the files fine -- in fact, once I have them, I can comment out that entire piece and the error still occurs.

Note that the script does not die at opening the file (line 30), and that the "for" loop and construction of the $file name variable is the same for both the FTP and the "open file" sections, but the script only dies when it attempts to read the file; the FTP portion works as expected.

I've tried all the obvious stuff: the files are not read-only, are in fact there, and can be opened and read if named explictly.

#!/usr/bin/perl -w
use strict;
use Net::FTP;
use Cwd 'abs_path';

my %species = (
    'Bacteroides_fragilis_NCTC_9434' => 'CR626927',
    );

my $home = abs_path(".");
my $datapath = "$home/srRNA";

mkdir $datapath unless (-e $datapath);
chdir ($datapath) or die;

foreach my $organism (sort keys %species) {
    my $ftp = Net::FTP->new("ftp.ncbi.nih.gov", Debug => 0) or die "Cannot connect to ftp.ncbi.nih.gov: $@";
    $ftp->login("anonymous",'-anonymous@') or die "Cannot login ", $ftp->message;
    $ftp->cwd("/genbank/genomes/Bacteria/$organism/") or die "Cannot change working directory ", $ftp->message;
    for ('.ptt', '.rnt') {
        my $file = $species{$organism} . $_;
        $ftp->get($file) or die "get $file failed ", $ftp->message;
    }
    $ftp->quit;
}

foreach my $organism (sort keys %species) {
    for ('.ptt', '.rnt') {
        my $file = $species{$organism} . $_;
        open (TAB, $file) or die "Can't open $file: $!\n";
        while (<TAB>) {
            next unless /^\d+\.\.\d+/;
        }
        close (TAB);
    }
}

Thanks for your help...
0
Comment
Question by:mjcoyne
  • 6
  • 4
10 Comments
 
LVL 39

Expert Comment

by:Adam314
ID: 20290467
In this line (28):
    for ('.ptt', '.rnt') {
Sets $_ to the same as '.ppt' and '.rnt' - not a copy of the value, but to the same thing (similar to a reference).  
So then the while statement (line 31) tries to change $_, but it can't.

0
 
LVL 39

Expert Comment

by:Adam314
ID: 20290490
eg:

my @list = qw(item1 item2 item3);

for (@list) {

	s/i/I/;

}
 

print join(",", @list) . "\n";  #Notice uppercase I

Open in new window

0
 
LVL 17

Author Comment

by:mjcoyne
ID: 20291352
I agree that $_ is responsible for this error, as it's the only read-only value involved in the script.  However, I can't grasp the difference -- especially in terms of $_ -- between these two constructs:

#!/usr/bin/perl -w
use strict;

my @files = ('CR626927.ptt', 'CR626927.rnt');

# this works
for (@files) {
    open (TAB, $_) or die "Can't open $_: $!\n";
    while (<TAB>) {
        print unless /^\d+\.\.\d+/;
    }
    close (TAB);
}

# this does not work
for ('CR626927.ptt', 'CR626927.rnt') {
    open (TAB, $_) or die "Can't open $_: $!\n";
    while (<TAB>) {
        print unless /^\d+\.\.\d+/;
    }
    close (TAB);
}

0
 
LVL 39

Accepted Solution

by:
Adam314 earned 500 total points
ID: 20291912
In the first one:
@files is an array variable, read/write
$_ is those values (eg: a C/C++ pointer to those elements), not a copy of their values
If you look at files after the "for (@files)", you'll see that it has changed.

In the second one:
$_ is now a pointer to a constant (the string 'CR626927.ptt' or 'CR626927.rnt')
The while loop is trying to change $_


Look at this version of your first code:
my @files = ('CR626927.ptt', 'CR626927.rnt');

# this works
$" = ",";   #just to make printing easier...
print "**\@files 1 = @files\n";   #@files is unchanged as of here
for (@files) {
    #Here, $_ is a pointer to the actual data in @files, not a copy of it
    open (TAB, $_) or die "Can't open $_: $!\n";
    while (<TAB>) {
        #The first time through the outer for loop, the first element (0th) in @files is changed
        #The second time through the outer for loop, the second element (1st) in @files is changed
        print "**\@files 2 = @files\n";
        print unless /^\d+\.\.\d+/;
    }
    close (TAB);
}

print "**\@files 3 = @files\n";  
#here:
#    $files[0] is the last line of CR626927.ptt
#    $files[1] is the last line of CR626927.rnt

0
 
LVL 17

Author Comment

by:mjcoyne
ID: 20292686
OK, maybe I'm being thick (likely).  But in your first answer (20290467), you said the reason the "for ('.ptt', '.rnt') {" construct *doesn't* work is because it "sets $_ to the same as '.ppt' and '.rnt' - not a copy of the value".  But, in your latest answer (20291912) you're saying the reason "for (@files) {" construct *does* work is because "$_ is those values...not a copy of their values".  Aren't these answers contridictory?

I know you're right, and that the problem is stemming from the difference between a value and a pointer to a value, but I'm failling to see some subtle difference here -- the value of $_ changes all the time without issues (for instance, the second time through the "for" loop), and I can't get why the "while (<TAB>) {" line triggers this error -- if the "for" loop can change the value of $_ (from CR626927.ptt to CR626927.rnt), why can't the "while" loop?

All "while" loops used in this manner constantly change the value of $_; it changes each time a new line is read -- after all, in the examples above, "print unless /^\d+\.\.\d+/;" is really saying "print $_ unless /^\d+\.\.\d+/;".

Thanks for your help.  I've already re-written the code to abolish the problem, but I'd like to more fully understand what caused this issue to rear its ugly head in the first place so I can avoid falling into the same trap again...
0
How to improve team productivity

Quip adds documents, spreadsheets, and tasklists to your Slack experience
- Elevate ideas to Quip docs
- Share Quip docs in Slack
- Get notified of changes to your docs
- Available on iOS/Android/Desktop/Web
- Online/Offline

 
LVL 39

Expert Comment

by:Adam314
ID: 20293105
In a for loop, $_ is a pointer to the values.  Changing $_ changes the original values.
In a while loop, the value of $_ is changed - it is not a pointer.

The reason it *doesn't* work in the "for ('.ptt', '.rnt')" and *does* work in the "for (@files)" is because
    In both cases, $_ is a pointer.
    @files is a variable that you *can* write to.  The while loop changes the values in @files.
    '.ptt' is a literal that you can *not* write to.  The while loop tries to change the value of a literal.
So, when $_ is a pointer to the literal '.ptt', you can not change it's value.  The while loop tries to set the value of $_, and this is the problem.
When $_ is a pointer to a value in @files, you can change it's value.  The while loop tries to change the value of $_, and it does - and this change is reflected in @files.

The previous example shows this, by having @files different after the loop.

Here is an example using subroutines instead.  When you pass parameters to a subroutine, you are actually passing pointers (perl handles all of this for you).  If you try to modify @_, you will be modifying the original parameter.  Normally, a subroutine makes a copy of all of it's parameters, either with shift, or @_.

sub TryToChange {
      #Subroutine actually gets a pointer to the passed parameters
      print "First parameter: $_[0]\n";
      $_[0] = 'Changed in TryToChange';
}


my $var = 'Original Value';
print "var=$var\n";
TryToChange($var);   # $var will be changed, because a pointer to $var was changed
print "var=$var\n";

TryToChange('literal');   # no problem, changing a copy of a literal
0
 
LVL 17

Author Comment

by:mjcoyne
ID: 20293851
OK, I'm getting it.  But, as your second-to-last example shows, @files is irreversibly changed by populating it with filenames, and then using it to open files:

.
<snip>
.
print "**\@files 3 = @files\n";  
#here:
#    $files[0] is [now] the last line of CR626927.ptt   # mjc: but it was CR626927.ptt originally
#    $files[1] is [now] the last line of CR626927.rnt   # mjc: but it was CR626927.rnt originally

So, this means you could only use this array of filenames once in this manner?  Say you wanted to use it to open files, as above, and then later on, use it with unlink to clean up.  It would fail, right?

It seems bizzare to me that even though @files is scoped outside of the "for" loop, the act of using it within a "for" loop destroys it for subsequent use.  It also is bizzare to me that I've never come across this Perl behavior before...

I suppose using "foreach my $filename (@files) {", which is my usual construct, would be the way to avoid this....
0
 
LVL 39

Expert Comment

by:Adam314
ID: 20296023
The @files is only affected if $_ is changed within the for loop.  

$"=", ";
my @files = ('file1.txt', 'file2.txt');
print "\@files=@files\n";
for (@files) {
      print "\$_=$_\n";
}
$_='new test';    #has no effect on @files
print "\@files=@files\n";     #Unchanged from before

As for "destroying"... it can be helpful.  If you want to change everyitem in an array:
    my @files = ('/path/to/file1', '/another/path/to/a/file2', '/one/more/path/to/a/file3');
    $_=basename($_) foreach (@files);   #This works because $_ is a pointer to the actual value, not a copy...
    print "files=@files\n";

Or:
    my @files = ('/path/to/file1', '/another/path/to/a/file2', '/one/more/path/to/a/file3');
    s|.*\/|| foreach (@files);    #This works because $_ is a pointer to the actual value, not a copy...
    print "files=@files\n";
0
 
LVL 17

Author Comment

by:mjcoyne
ID: 20297372
I'll admit it's still troubling to me that an array is altered merely by iterating through it...

However, I understand that this behavior is also the source of my  "Modification of a read-only value..." error, since Perl obviously needs a write permissible variable under the circumstances.

Thank you, Adam.
0
 
LVL 39

Expert Comment

by:Adam314
ID: 20305707
>>array is altered merely by iterating through it...
Only if you change the iterated variable inside of the loop.

0

Featured Post

Threat Intelligence Starter Resources

Integrating threat intelligence can be challenging, and not all companies are ready. These resources can help you build awareness and prepare for defense.

Join & Write a Comment

I've just discovered very important differences between Windows an Unix formats in Perl,at least 5.xx.. MOST IMPORTANT: Use Unix file format while saving Your script. otherwise it will have ^M s or smth likely weird in the EOL, Then DO NOT use m…
On Microsoft Windows, if  when you click or type the name of a .pl file, you get an error "is not recognized as an internal or external command, operable program or batch file", then this means you do not have the .pl file extension associated with …
Explain concepts important to validation of email addresses with regular expressions. Applies to most languages/tools that uses regular expressions. Consider email address RFCs: Look at HTML5 form input element (with type=email) regex pattern: T…
Polish reports in Access so they look terrific. Take yourself to another level. Equations, Back Color, Alternate Back Color. Write easy VBA Code. Tighten space to use less pages. Launch report from a menu, considering criteria only when it is filled…

762 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

16 Experts available now in Live!

Get 1:1 Help Now