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

Is there any way to start a foreach statement at an arbitrary position? Obviously, there is a 'for' statement like this:
my @array=qw( the that which who when if this); my $size=$#array+1; for(my $i=2;$i<$size;$i++){ $array[$i]=uc $array[$i]; }
This is okay, but it can get kind of clunky to manipulate an array element by its index.

Replies are listed 'Best First'.
Re: Starting foreach at an arbitrary position
by jdporter (Paladin) on Jan 16, 2003 at 20:19 UTC
    What are the criteria for the elements you want to process? You want to do the last half of the list, for example? You can use other functions to select some subset of the array. One way is to use an array "slice" to get elements at arbitrary indices:
    foreach ( @array[ 5, 9, 16..35 ] )
    You get the picture...

    jdporter
    The 6th Rule of Perl Club is -- There is no Rule #6.

Re: Starting foreach at an arbitrary position
by larsen (Parson) on Jan 16, 2003 at 20:27 UTC
    You can use an appropriate slice, like this:
    my @array = qw/the that which who when if this/; foreach( @array[2 .. $#array] ) { $_ = uc $_; # $_ is an _alias_ } print "@array\n";
    Unfortunately you have to use $#array (I'd like if I could write things like @array[2:] but after all this is syntactic sugar). Note that you can't write 2 .. -1 to mean from the element with index 2 to the last one.
Re: Starting foreach at an arbitrary position
by MarkM (Curate) on Jan 16, 2003 at 20:48 UTC

    Since other people seem to only be suggesting the 'slice' implementation, I will follow in the spirit of Perl, and suggest a persuasive alternative:

    for (my $i = 2; $i < @array; $i++) { local *_ = \ $array[$i]; ... use $_ as you would normally use it in foreach() ... }

    The "local *_ = \ $scalar" trick makes $_ become an alias to $scalar (really the value in $scalar). This is equivalent to the effect that foreach() has, with the exception that this method does not work for lexical variables. UPDATE: Further explanation: foreach() accepts a lexical iterator variable. local(*) can only be used on global variables.

    When would you use this alternative over the slice alternative? I would suggest that the slice alternative be used for all relatively simple operations that involve few array elements (<1000), and that can be easily represented using '..' notation. The slice solution is certainly more common, and therefore, easy to maintain.

    The most significant problem with the slice alternative is that array slices are actually a list of scalars specified using a simplified notation. You can easily specify @array[1 .. 1000000], but the result is a temporary list that has 1000000 scalars in it. Unless you have relatively unlimited RAM, the alternative presented above may be for you. (NOTE: I wouldn't consider 100 elements to be large enough to avoid using '..' and an array slice)

    A major benefit for the alternative presented above, is that the algorithm used to move through the array can be arbitrarily complicated. Backwards, forwards, skip by 2, etc. .

    Have fun... :-)

      The point about the advantages of local *_ = \ $array[$i]; is well taken, but I absolutely hate for(;;) loops. It seems like I've made a fencepost error almost every time I wrote one of those. There are very few cases where it's the most appropriate solution. Since for(;;) is implemented in terms of while(){} continue{}, you should use while(){} unless you actually need a very simple continue{} block (and I've never come across such a case). For abitrarily complex index calculation I'd do something like this:
      my $get_next_idx = make_index_iterator(\@array); while(my $i = $get_next_idx->()) { local *_ = \ $array[$i]; # $_ is aliased here }
      (Note how the point about ($_) for BrowserUk's code doesn't apply as $i is an array index, not an array element.)

      Makeshifts last the longest.

      The "local *_ = \ $scalar" trick makes $_ become an alias to $scalar (really the value in $scalar). This is equivalent to the effect that foreach() has, with the exception that this method does not work for lexical variables.

      What do you mean when you say "this method does not work for lexical variables?" It will work just fine as a way to iterate over lexically declared arrays as well as global arrays. There is nothing wrong with using a typeglob to alias a lexical, is there?

      I don't think you meant to imply otherwise but that statement confuses me.

      $ perl -Mstrict -wle 'my @lexical = (1..3); for (my $i=0; $i<@lexical; + $i++){ local *_ = \$lexical[$i]; $_ = "foo"} print "@lexical"' foo foo foo

      Update: Upon re-reading that, I'm guessing that you simply mean that you can't use a lexical as the iterator variable.

      -sauoq
      "My two cents aren't worth a dime.";
      

        I think what he meant was that the LHS can't be a lexical variable.


        Warning: Unless otherwise stated, code is untested. Do not use without understanding. Code is posted in the hopes it is useful, but without warranty. All copyrights are relinquished into the public domain unless otherwise stated. I am not an angel. I am capable of error, and err on a fairly regular basis. If I made a mistake, please let me know (such as by replying to this node).

Re: Starting foreach at an arbitrary position
by BrowserUk (Patriarch) on Jan 16, 2003 at 21:29 UTC

    Another alternative is to use an iterator

    sub getIter{ my ($start,$end, $aryRef) = @_; $end=$#$aryRef if $end==-1; return sub { return $aryRef->[$start++] if $start <= $end; (); } } my @a = 1 .. 1000000; my $iter = getIter 500000, 500005, \@a; while ( $iter->() ) { print $_, $/; }

    gives 500001 500002 500003 500004 500005 500006 </code>

    No long lists created, just discard the iterator when your done with it, have as many active simutaneously as you need and it's a fairly trivial modification to make it move in steps or backwards etc.


    Examine what is said, not who speaks.

    The 7th Rule of perl club is -- pearl clubs are easily damaged. Use a diamond club instead.

      Your iterator function has a few problems with it.

      First, the code does not give the output you suggest. To make your code work properly, the while condition should read:

      while (($_) = $iter->()) {

      The () around $_ is necessary as $iter->() returns () when the iterator is done.

      Second, the code does not allow foreach()-like behaviour. $_ is not aliased to the array element value and does not allow expressions such as the following to have effect: (from the original node)

      $_ = uc $_;

      Third, the range operator ('..') still creates a list that is 1000000 scalars long, and copies this list into the array. Try the following code, and watch your machine quickly die:

      $ perl -e 'push(@a, [1 .. 1000000]) while 1'

      (NOTE: The range operator optimization only applies when used directly from a foreach loop. For example: 'for (1 .. 1000000) { ... }')

      All in all, a for(;;) loop with a 'local *_ = \ $array[$i]' is far more effective and efficient... :-)

        Your right about the output. I originally typed the test code into a perl shell like so.

        C:\test>p1 perl> sub getIter{ my ($s,$e,$ref)=@_; $e=$#$ref if $e==-1; return sub + { $s<=$e ? $ref->[$s++] : (); }} perl> @a = 1 .. 1000000 perl> $iter = getIter 500000, 500005, \@a perl> print while $_ = $iter->() 500001500002500003500004500005500006 perl>

        Which, whilst it proved that the basic idea worked, wasn't very palitable. So, being lazy, I C&P'd from the screen and modified it in the edit box. Bad move.

        BTW. You forgot to mention that the condition needs to be "<" not "<=" :^).

        However, making the above code into a normal program like so

        #! perl -slw use strict; sub getIter{ my ($start, $end, $aryRef) = @_; $end = $#$aryRef if $end == -1; return sub { return $aryRef->[$start++] if $start < $end; (); } } my @a = 1 .. 1000000; my $iter = getIter 500000, 500005, \@a; print while $_ = $iter->(); __END__ 500001 500002 500003 500004 500005

        Does produce the output I indicated.

        You can take it a step further step further using *_.

        #! perl -slw use strict; sub getIter{ my ($s, $e, $ref)=@_; $e = $#$ref if $e == -1; return sub { ($s < $e) ? *_ = \$ref->[$s++] : return; } } my @a = 1 .. 1000000; my $iter = getIter 500000, 500005, \@a; print while $iter->(); $iter = getIter 500000, 500005, \@a; $_ = 'modified' while $iter->(); $iter = getIter 500000, 500005, \@a; print while $iter->(); __END__ C:\test>227478 500001 500002 500003 500004 500005 modified modified modified modified modified C:\test>

        Which does alais the array being iterated and allow write operations. However, it stomps on $_ which a for loop doesn't.

        #! perl -slw use strict; $_ = 'some value'; print; for (1 .. 10) { print; } print; __END__ C:\test>temp some value 1 2 3 4 5 6 7 8 9 10 some value C:\test>

        I am unaware of any way of emulating perl's behaviour in localising $_ in your own iterators unfortunately.

        You can wrap the use of the iterator in a bare block and localise $_ for the duration of the scope of use, which is effectively what you are doing, though the for loop block provides a naturally convienient scope rather than having to create one artificially.

        With regard to the other two matters.

        Why

        while ( ($_) = $iter->() ) { ...

        rather than

        while ($_ = $iter-() ) { ...?

        As for the range operator building a list in list assignment. Yes, of course it does, but it is just a convenient way to set up some test data and doesn't having any bearing on the use of the iterator?


        Examine what is said, not who speaks.

        The 7th Rule of perl club is -- pearl clubs are easily damaged. Use a diamond club instead.