Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

I am wanting to verify the existence of various strings within a text file, & the order of these strings is not mandated.

I have implemented a loop which looks for each successive string. However, there is no need to look for strings already encountered, so I would like to remove strings from the search list as they encountered.

splice() seemed to be the obvious choice to delete elements within the array, or so I thought. The following simplified code executed under Perl 5.8.8 doesn't seem to delete any elements within the search list:

#!/bin/env perl use strict; use Data::Dumper; use constant REQUIRED_FIELDS => qw/zero one two three four five six se +ven/; my $a = [qw/three one two/]; print Data::Dumper->Dump([$a], ['a']); verify($a); exit; sub verify { my $a = shift; my $fields = [REQUIRED_FIELDS]; my $href = {}; foreach my $e (@$a) { my $i; foreach my $field (@$fields) { if ($e eq $field) { $href->{$field} = 0; splice @$fields, $i, 0; print Data::Dumper->Dump([$i, $fields], [qw/i fields/] +); } ++$i; } } print Data::Dumper->Dump([$href], [qw/href/]); }

Execution yields the following output:

$ perl test.pl $a = [ 'three', 'one', 'two' ]; $i = 3; $fields = [ 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven' ]; $i = 1; $fields = [ 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven' ]; $i = 2; $fields = [ 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven' ]; $href = { 'three' => 0, 'one' => 0, 'two' => 0 }; $
Is the array used by foreach cached? I am not understanding why splice is not pruning the found values.

Thanks!

Replies are listed 'Best First'.
Re: altering array in foreach during execution?
by ikegami (Patriarch) on Dec 02, 2011 at 22:36 UTC

    The list of elements over which foreach iterates is on calculated before foreach starts. Changing the number of elements in the array should have no effect on the looping whatsoever.

    Not what you want.

    In practice, due to an optimisation and the fact that the stack isn't refcounted, changing the array over which you are iterating can cause the wrong values to be seen and fatal errors ("Use of freed value in iteration").

    Not what you want.


    You could use loop over the indexes (and use last as you should have been).

    my @fields = REQUIRED_FIELDS; for my $e (@$a) { for my $i (0..$#fields) { if ($e eq $fields[$i]) { $href->{$e} = 0; splice @fields, $i, 1; last; } } }

    But a hash would be better.

    my %fields = map { $_ => 1 } REQUIRED_FIELDS; for my $e (@$a) { $href->{$e} = 0 if $fields{$e}; }

    In fact, since the hash never changes, you only need to create the hash once, not every time verify is called.

    Update: Fixed the OP's misuse of splice as per AnomalousMonk's post.

      Wow! Thanks!

      Once again, dealing with the converse ends up creating a far simpler solution.

      And from the standpoint of Perl, hashes can really be one's friend.

      Thanks, again for taking the time to answer my questions!

Re: altering array in foreach during execution?
by AnomalousMonk (Archbishop) on Dec 02, 2011 at 23:07 UTC
    splice @$fields, $i, 0;

    Furthermore, and notwithstanding that a hash-based approach is, indeed, better for what you seem to want to do, the quoted splice statement from the OPed code also does not do what you want, removing zero elements (LENGTH == 0) beginning at an OFFSET of  $i in the array, i.e., it's an expensive no-op.

    >perl -wMstrict -le "my $ar = [ qw(a b c d) ]; ;; my $i = 0; for my $e (@$ar) { $e = uc $e; splice @$ar, $i, 0; ++$i; } ;; use Data::Dumper; print Dumper $ar; " $VAR1 = [ 'A', 'B', 'C', 'D' ];

    (As a reinforcement to ikegami's point that you really don't want to add or delete elements in an array over which you are iterating, try changing the 0 in the  splice statement in the example code of this reply to a 1.)

      Oops, you're right. I did specific zero elements to be deleted. My bad.

      My appreciation to both of your replies for setting me on a better path.

Re: altering array in foreach during execution?
by Marshall (Canon) on Dec 03, 2011 at 04:15 UTC
    Maybe I don't understand the question, but it appears to me that you want the list of REQUIRED_FIELDS minus the ones that are in your $a which is a reference to an array?

    I personally would use a different return value, either a list or an array reference as the output rather than pervert (grossly modify) the input to such an extent that it no longer means what it did as the input.

    I mean if the input is "here's what I have" and the output is "here's what you didn't have" - those are two very different things.

    It looks to me like the output of verify() is of a different "type" or "unit" than the input. I would return a list like I did, or a reference to an array of the "new type".

    #!/usr/bin/perl -w use strict; use constant REQUIRED_FIELDS => qw/zero one two three four five six se +ven/; my $a_ref = [qw/three one two/]; my @missing_fields = verify($a_ref); print "missing fields: @missing_fields\n"; sub verify { my $a_ref = shift; my %seen = map{$_ => 1}@$a_ref; return (grep{!$seen{$_}}REQUIRED_FIELDS); } __END__ Prints: missing fields: zero four five six seven