Beefy Boxes and Bandwidth Generously Provided by pair Networks
Keep It Simple, Stupid
 
PerlMonks  

Inserting into an array while stepping through it

by Anonymous Monk
on Jun 13, 2003 at 02:15 UTC ( [id://265555]=perlquestion: print w/replies, xml ) Need Help??

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

I have a basic "foreach" loop on an array. If the element of the array meets a condition it is handed to a subroutine that may return either a scalar or a list which should replace that element of the array.

If its a list then splicing it in upsets the indexing of the loop iterator.

Any ideas on how this can be elegantly solved?

  • Comment on Inserting into an array while stepping through it

Replies are listed 'Best First'.
Re: Inserting into an array while stepping through it
by Zaxo (Archbishop) on Jun 13, 2003 at 02:25 UTC

    You can do that by taking the indexes in reverse order. Added slots in the array move the higher indexes, but leave the lower ones alone.

    for ( reverse(0..$#array) ) { if ( the_condition($array[$_]) ) { splice @array, $_, 1, the_sub($array[$_]) } }

    After Compline,
    Zaxo

      Yes, Zaxo, I know that doing backwards works.

      However this is part of a parser and the constraints of where it is used means that the traversal hasto be forward.

Re: Inserting into an array while stepping through it
by sauoq (Abbot) on Jun 13, 2003 at 02:35 UTC
    for ( 0 .. $#array ) { my $element = shift @array; push @array, $elt eq 'condition' ? my_sub( $elt ) : $elt; }

    Update: As suggested by Skeeve in his reply below, I've written an explanation in my reply to him.

    -sauoq
    "My two cents aren't worth a dime.";
    
      Don't you think you should explain that a bit?

      Not to me, I understand it (you rotate the whole array once) but to all those new monks reading the solutions given here. Your solution isn't one people often see when they are used to less comfortable languages ;-) And it's also a solution that's quite different from all the others in this thread.

        Don't you think you should explain that a bit?

        Hrm. Well, I guess I didn't think I should, but you've made me question that. ;-) It's pretty straight forward, I think. And it wouldn't take much experimentation to see what is going on, even if someone couldn't tell immediately.

        But, I don't mind...

        I wrote the loop as 0 .. $#array and, as a style issue, I don't really like that. I think it would be clearer to say 1 .. @array but, as I don't use $_ at all it doesn't much matter. It will execute the body of the loop as many times as there are elements in @array when the loop starts. That last is important because we may change the size of the array inside the loop but that won't change how many times we loop.

        Inside the loop, I use shift to remove an element from the front of the array. Then I get ready to push something back onto the end of the array. What we push is determined by whether our element meets a condition. If it does, I call my_sub() and the return value of my_sub() gets pushed onto the array. If it doesn't match the condition, then I push the element itself back onto the array. When the loop is done, we've processed each element in the original array exactly once.

        There is one other important thing to notice. The subroutine, my_sub(), could return more than one value. That's fine. The push builtin takes an array as its first argument and a list of additional arguments and pushes all of the additional arguments onto the array. That's how the array can grow. Also, since push takes a list, the subroutine will execute in list context. That might be necessary for it to know that it should return a list.

        I think that about explains it... any questions?

        -sauoq
        "My two cents aren't worth a dime.";
        
Re: Inserting into an array while stepping through it
by bobn (Chaplain) on Jun 13, 2003 at 02:30 UTC
    foreach isn't what you want here. Assuming you don't want to examine what you've just inserted:
    my @a = qw( 1 3 2 4); # NOT TESTED MUCH $i = 0; while ($i < @a ) { if ($a[$i] == 2) { my @in = mysub(whatever); splice @a, $i, 1, @in; $i += @in ; } else { $i++; } } print "@a\n"; sub mysub { # whatever return ( qw(9 2 9)); # always return an array, even if only 1 element }
    Update: removed parens in $i += ( @in ); though it seems to work.

    --Bob Niederman, http://bob-n.com
      That works fine, Bob.

      I was hoping to avoid using an index. I'm of the school of thought that the fewer variables a program has - especially global variables, see http://c2.com/cgi/wiki?GlobalVariablesAreBad - the easier it is to understand and maintain.

      Still, this gets me over that hurdle ;-)

        I just wanted to point out that the index ($i) does not have to be global. You could write:

        while (my $i < @a)

        ... and have $i scoped to the while loop. Check out Lexical scoping like a fox.

Re: Inserting into an array while stepping through it
by meredith (Friar) on Jun 13, 2003 at 02:32 UTC
    Maybe a good use for the function map. Layman's terms: It applies a sub to each item of a list, and it returns a list of all the sub results. Okay, a better example: You have a list with numbers, you want a list of each number, times 3.
    my @list = ( 5, 11, 7); my @timesthree = map { $_ *= 3 } (@list);
    or if you only want to operate on multiples of five:
    my @timesthree_onlyfives = map { $_ *= 3 if ($_ % 5 == 0) } (@list);
    BUT, you will get a list of one item! so you want:
    my @all_timesthree_onlyfives = map { if ($_ % 5 ==0) { $_ *= 5; } else { return $_; } } (@list);
    But that could be done better.

    Hope I've described it well, and HTH!

    mhoward - at - hattmoward.org

      Well, map() is a good alternate solution (though it wouldn't be doing the modification in place exactly.) But, you didn't show how to use it in the way he wants to...

      @list = map { $_ eq 'condition' ? my_sub( $_ ) : $_ } @list;
      and yes, that'll work when my_sub() returns a list too.

      -sauoq
      "My two cents aren't worth a dime.";
      
        Thanks for the code -- I just wanted to include an explanation for the code I posted, instead of a one-liner and some code. Anonymous Monk sure posts a lot of questions, and I want to help him learn. ;) Besides, everyone knows that using map scores you extra Perl Points(tm)!!

        mhoward - at - hattmoward.org
Re: Inserting into an array while stepping through it
by BrowserUk (Patriarch) on Jun 13, 2003 at 02:49 UTC

    Processing the array in reverse as Zaxo suggests is probably the best way, unless you need to process it forwards.

    If that's the case, you could reverse the array, reverse the list in the for and then reverse the array again when you've finished. reverse is quite amazingly efficient, so the overhead is lower than you might expect.

    @array = reverse @array; for ( reverse 0.. $#array ) { splice @array, $_, 1, func( $array[$_] ); } @array = reverse @array;

    Or you could replace the element of the array by a reference to the returned list and then flatten the array when the loop is complete.

    for( 0 .. $#array ) { my @results = func( $array[$_] ); $array[$_] = @results > 1 ? $results[0] : \@results; } @array = map{ ref $_ ? (@$_) : $_ } @array;

    It would be interesting to see which approach is the more efficient.


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller


      Or you could replace the element of the array by a reference to the returned list and then flatten the array when the loop is complete

      I liked that idea too. GMTA

      I expect it would also be faster (too lazy to test) as it is a classic link list solution and passes a minimal amount of data around, etc, etc. It will use roughy double the memory though when you flatten the list...

      cheers

      tachyon

      s&&rsenoyhcatreve&&&s&n.+t&"$'$`$\"$\&"&ee&&y&srve&&d&&print

Re: Inserting into an array while stepping through it
by runrig (Abbot) on Jun 13, 2003 at 02:26 UTC
    This is one instance where a C-style for loop might be better:
    for (my $i=0; $i <= $#array; $i++) { #stuff with $array[$i] }
    And you probably want to increment i by the size of the arrays you splice in when they get spliced in.
      I tried that.

      Suppose my orginal array is
      ( a, aba, aaa, aca, bbb, abc )
      Suppose my trigger for change is when the elements of the line are all the same - "aaa".

      So index goes 0, 1, 2

      At index = 2 I splice in 3 lines to replace it, so my array now looks like

      ( a, aba, replacement1, repalcement2, replacement3, aca, bbb, abc)

      Next iterator is index=4, which gives me "replacement2" instead of "aca".

      If I'm using something like:

      splice(@array, $i, 1, func($array[$i]);
      (which I am) and it can return a list the size of which is going to depend on context (such a hw previous eleements of the array had been treated - think of an order form where discount is by group and volume), then the juggling of the index counter is pretty hairy. I've tried this. The resulting code is very far from elegnant and my test harness easily "breaks" it, not least of all when the returned value is an empty list and the primary array gets shrunk. The iterator misses a line! However the worst case is when the replacement is two elements and the second element is a copy of the original. (Think about that!)
        I don't see the problem. The iterator is bumped to 4, but at the beginning of the next iteration, it'll be 5. You could even put the 'bump' into a continue block (assign '2' to $bump, but don't increment $i untill the continue block). If the array is size zero, then it gets bumped by -1. The only problem I see is directly splicing in a function returning an array. You couldn't do that; you'd have to return a temp array first, get the size, then splice in the temp array. I don't understand what you say about splicing in a copy of the original. Do you want to recursively replace elements or not? I was assuming not. If you do, then I'd change my answer (and alot of other answers in this thread would have to change also).

        But I see lots of other good answers in this thread anyway :)

        Update: Correction, you can't have a continue block with a C-style for loop. But here's some example code which works:

        my @arr = qw(abc aaa ccc bbb ddd); for (my $i=0; $i<=$#arr; $i++) { my @tmp = replace($arr[$i]); splice @arr, $i, 1, @tmp; $i += $#tmp; } print "@arr\n"; sub replace { local $_ = shift; return /^aaa$/ ? qw(rep1 rep2 aaa) : /^bbb$/ ? () : $_; }
Re: Inserting into an array while stepping through it
by tachyon (Chancellor) on Jun 13, 2003 at 02:37 UTC

    You could do it like this by inserting array refs as you go then flatten it back to a plain list afterwards.

    my @ary = ( 1, 2, 3 ); for (@ary) { $_ = check($_); } # now flatten the list of mixed array refs and scalars into one list my @tmp; push @tmp, ref $_ ? @$_ : $_ for @ary; @ary = @tmp; print "$_\n" for @ary; sub check { return $_[0] == 2 ? [ 'a', 'b', 'c' ] : $_[0]; } __DATA__ 1 a b c 3

    cheers

    tachyon

    s&&rsenoyhcatreve&&&s&n.+t&"$'$`$\"$\&"&ee&&y&srve&&d&&print

Re: Inserting into an array while stepping through it
by benn (Vicar) on Jun 13, 2003 at 09:57 UTC
    In the spirit of TMTOWTDI, you could also simply use a temporary array...
    my @arr = (1,2,3,4,5,6); my @tmp; foreach (@arr) { push @tmp, ($_ > 2) ? munge($_) : $_; } @arr = @tmp; print @arr; # 1 2 3 x y z 5 x y z sub munge { ($_[0] % 2) ? $_[0] : ("x","y","z"); }
    ....but personally, I'd use a map, as suggested above.

    Cheers, Ben.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://265555]
Approved by Zaxo
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others exploiting the Monastery: (7)
As of 2024-04-19 16:35 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found