last iteration of a for loop

Discussion in 'Perl Misc' started by hymie!, Feb 26, 2014.

  1. hymie!

    hymie! Guest


    So I've got a for loop. It's doing some printing, and at the end of
    what it prints, it prints a separator line like


    Is there an easy way that I can tell my loop "don't print the separator
    after the last iteration"?

    I know I can say
    print "----------" unless $i==9;
    but I was hoping there might be something more robust or generic, or that
    would work in a more complex loop such as
    foreach $day (sort { daynum($a) <=> daynum($b) } keys %sched)

    Alternately, maybe I can print the separator at the **top** of my
    loop, except on the **first** iteration? I can probably do that
    with a flag, but again, I was hoping for something more robust and

    hymie!, Feb 26, 2014
    1. Advertisements

  2. Am 26.02.2014 15:30, schrieb hymie!:

    a way for a
    foreach my $item (@not_changing_list) {
    } loop would be too rewrite it as a: [untested]

    for (my ($item, @l) = @not_changing_list; $item = shift @l; @l > 0) {
    print "--------\n" if @l;
    # @l is empty, so false when processing last item

    It has obv the disadvantage that the original list has first to be
    copied, but looks anyway solid unless we are talking about massive data
    sizes or side effects (e.g. when working with Tie::File).

    Janek Schleicher, Feb 26, 2014
    1. Advertisements

  3. This is a very typical scenario and it baffles a surprisingly large
    number of people. There are 2 standard ways of handling it.

    If it is just output you want to combine then maybe you can use the
    join() function:

    foreach ($elem in @list){
    push @results, process_item($elem);
    print (join($separator, @results));

    Or you can process the first element separately (easier than the last
    because often you don't know when you have reached the last element):

    foreach ($elem in @list[1..@list]){
    print $separator;

    Or with HOFs you can use a standard reduce.

    Jürgen Exner, Feb 26, 2014
  4. Am 26.02.2014 16:50, schrieb Janek Schleicher:
    should be of course
    for (my ($item, @l) = @not_changing_list; @l > 0; $item = shift @l) {

    (use C-stylish for loops so rarely)

    A complete different way that works more generic would be to use one of
    the Iterator modules from CPAN, e.g.

    use Array::Iterator;

    for (my $i = Array::Iterator->new(@array); $i->has_next(); $i->next()) {
    my $current = $i->current();
    # .. do something with current
    print "------" if $i->has_next();

    in that way can also work with non constant lists.

    Janek Schleicher, Feb 26, 2014
  5. AFAIK, you've hit a fundamental, conceptual limitation in the way
    "structured programming loops" are defined: The notion of "some piece of
    code which gets executed prior to each re-execution of the loop body"
    doesn't exist. It is possible to work around this for do - while loops
    with goto a la

    goto doit;
    do {
    # print separator

    # print other stuff
    } while keep_on_running();

    but that's not a good idea in Perl because Perl gotos are very expensive
    and it doesn't work for loops which check the condition prior to
    executing the loop-body for the first time.

    The usual workarounds would be "print separator at the top and 1st
    element before the loop" or "print separator at the bottom and first
    element after the loop".
    Rainer Weikusat, Feb 26, 2014
  6. That equivalent to the print .... if $i == 9 the author wanted to get
    rid of (in both cases and besides being an atrocious attempt if
    imitating Java in Perl for the sake of 'eyesore' ...).
    Rainer Weikusat, Feb 26, 2014
  7. [...]
    Rainer Weikusat, Feb 26, 2014
  8. Am 26.02.2014 17:13, schrieb Rainer Weikusat:
    You mean beside, not having to track the not needing loop nr and beside
    having magical nrs in your code, beside it works easy when changing the
    list in any way that is not too drastic, or to say it with OP's words:

    I know I can say
    print "----------" unless $i==9;
    but I was hoping there might be something more robust or generic, or that
    would work in a more complex loop such as
    foreach $day (sort { daynum($a) <=> daynum($b) } keys %sched)

    Both my proposed solutions would work easy (to implement, read, maintan,
    adopt), robust, generic with that and even if you got a more complex
    question (like an XML feed or a complex asynchronous calculation), just
    take another CPAN module for Iterators, usually all you have to give is
    a sub of how to get the next elem and here indeed wheter the next elem
    even exists. But you certainly can get rid of having to hard code it in
    the loop.

    I don't know what you expected,
    I mean at some point we have to say to Perl or control flow, that a part
    of the loop only has to be executed if it is not the last round. I think
    a if @l or a if @i->has_next is close to be shorter than any
    possible other syntactic sugar or any hack.

    I mean, we can even make something like:
    my ($first, @inbetween, $last) = @l;
    HANDLE_INBETWEEN: for ... {
    what is pretty much what you proposed.

    Pretty much this is what OP asked for (and maybe a compiler might
    optimize us too),
    all I give him is a kind of 2 shortcuts for that, that still ain't to be
    hacks, still are maintainable and readable and where we don't probably
    copy 3x relevant code.

    Of course, that neither can handle all kind of loops (just if it could,
    Perl wouldn't be Turing complete), nor it is a quicker/shorter than to
    hardcode list abbrubt condition into the loop.

    Make a better suggestion, (beside goto xor make 1st and last round
    seperate special cases)
    Janek Schleicher, Feb 26, 2014
  9. hymie!

    Dr.Ruud Guest

    Yes, rather go for that.

    perl -wE'
    sub show {
    my $sep = ("- " x 6)."-";
    my $show_sep;

    for my $i ( 1 .. 4 ) {
    $show_sep and say $sep or $show_sep = 1;
    say $i;
    Dr.Ruud, Feb 26, 2014
  10. hymie!

    Tim McDaniel Guest

    As an aside, this is an instance of "loop unrolling".
    "HOFs"? Googling just shows that there's a town named Perl in
    Saarland, where German is prevalent, and so there are lots of places
    called .* Hof (.* House).
    Tim McDaniel, Feb 26, 2014
  11. I 'mean' that it is special-casing the last iteration of the loop in
    exactly the same way as the original counting loop, just adapted to a
    different set of 'iteration mechanics' you happen to like better than
    ordinary Perl-foreach loops. For the 'traverse a list case', this can also
    be written as

    my @temp = produce_list_of_elements();
    for (0 .. $#temp) {
    # do something with $temp[$_]
    print_sep() unless $_ == $#temp;

    No need to count anything explicitly, no 'magic constants' and no for
    (;;;) loop expressing something semantically equivalent in a more
    complicated way. But since that's still what the OP didn't want to do
    (and I'm convinced he is that knowledgable himself), I didn't post this.

    I didn't propose anything, I just pointed out that a ready-made solution
    doesn't exist and mentioned two other workarounds.
    Rainer Weikusat, Feb 26, 2014
  12. I don't know of a clean solution, but have often wanted one. An "again"
    block similar to the "continue" block, which only gets executed once it
    is decided the loop actually will get executed again, would be nice.
    (Whether it gets executed on "next" or not is an open question, I would
    think not.)

    I usually just use the construct
    join "whatever", map {} ...

    But that is certainly inconvenient when that data doesn't fit in memory
    nicely, or should stream smoothly.

    Xho Jingleheimerschmidt, Feb 27, 2014
  13. hymie!

    John Black Guest

    Isn't that what "redo" does?

    John Black
    John Black, Feb 27, 2014
  14. Not that I know of. The redo statement repeats the loop without
    re-evaluating whether it ought to be repeated, and there is no redo block.

    perl -le 'for (1..10) {print $_;} continue {print "-----"}'

    a redo statement after the 'print $_;' sends this into an infinite loop,
    and a continue statement does not alter the behavior from original.

    Xho Jingleheimerschmidt, Feb 27, 2014
  15. hymie!

    Tim McDaniel Guest

    It would be possible for a loop like
    for VAR (LIST)
    because it appears that LIST is evaluated once when the loop starts,
    so it knows the list of values to be iterated over.

    In general (for (EXPR1; EXPR2; EXPR3) or while (EXPR)), I don't see
    how the loop structure could know whether it's on the last iteration
    or not, because there might be global side effects or dependencies on
    the loop iteration and/or termination.
    Tim McDaniel, Feb 27, 2014
  16. hymie!

    Ted Zlatanov Guest

    On 26 Feb 2014 14:30:26 GMT (hymie!) wrote:

    h> So I've got a for loop. It's doing some printing, and at the end of
    h> what it prints, it prints a separator line like

    h> ----------

    h> Is there an easy way that I can tell my loop "don't print the separator
    h> after the last iteration"?

    Easiest way IMO, and a useful idiom regardless:

    #+begin_src perl


    use warnings;
    use strict;

    my @list = sort keys %ENV;

    while (my $key = pop @list)
    print "$key\n";
    print "not last\n" if scalar @list;
    print "last\n" unless scalar @list;

    Ted Zlatanov, Feb 27, 2014
  17. redo re-executes the loop body without evaluating the condition again.
    That's a loop control 'verb' completely unrelated to the sought-after
    feature. Perl has a continue-block which is executed after the loop body
    and before the condition is evaluated. What would be needed here would
    be a block which is executed after the loop body and after evaluating
    the condition resulted in the descision to execute the loop body for
    another time. This could be simulated in Perl like this:

    my $repeated;
    while (condition()) {
    if ($repeated) {
    # special block goes here

    # ordinary loop body
    $repeated = 1;

    but this is really a bad implementation because it keeps assigning to
    the indiciator variable and keeps retesting its value despite it'll
    never change after the first iteration. A good implementation (in this
    sense) would be

    if (condition()) {
    goto body;
    do {
    # special block

    # loop body
    } while (condition());

    but this is really a transformation which should be performed
    automatically by a compiler and certainly shouldn't be implemented in
    Perl. Since this is a common construct, a "high-level" syntax for
    expressing it would be helpful, ie one which neither relies on explicit
    gotos to indicate the desired control flow nor on mostly useless code.
    Rainer Weikusat, Feb 27, 2014
  18. hymie!

    Justin C Guest

    Maybe it's subtle, but I don't see a difference between the
    above and the below:

    print "not last\n" if @list;
    print "last\n" unless @list;

    Justin C, Feb 27, 2014
  19. hymie!

    Ted Zlatanov Guest

    JC> Maybe it's subtle, but I don't see a difference between the
    JC> above and the below:

    JC> print "not last\n" if @list;
    JC> print "last\n" unless @list;

    The scalar context is applied automatically, so there is no difference

    Having been bitten by this in the past (non-scalar context applied when
    I wanted scalar), I prefer to always be explicit. Similarly, I use
    "shift @_" and "shift @ARGV" explicitly in most situations. Both of
    these are probably too cautious for typical Perl usage today.

    Ted Zlatanov, Feb 27, 2014
  20. Am 27.02.2014 18:31, schrieb Ted Zlatanov:
    Of course, that's both personal style.

    I agree to you, that you try to avoid unclear spots,
    but there aren't anyone in boolean context.

    For me, if there is a scalar ... line in source code, I always think
    a) that is important
    b) it can't be solved otherwise
    c) something is wrong with our design, as we don't use the objects

    Janek Schleicher, Feb 27, 2014
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.