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

Any suggestions as to condensing/optimizing the following code fragment:

foreach my $i ( $from .. $to ) { if ($i % 2 != 0) { next; } elsif ($i % 3 != 0) {next; } elsif ($i % 4 != 0) {next; } elsif ($i % 5 != 0) {next; } else { say $i;} }

Replies are listed 'Best First'.
Re: if ... elsif ... else
by hippo (Archbishop) on Jun 01, 2017 at 08:52 UTC

    You could do this if you just want less typing:

    foreach my $i ( $from .. $to ) { next if ($i % 2 || $i % 3 || $i % 4 || $i % 5); say $i; }

    Or did you want some programmatic way to represent the range of mod arguments?

Re: if ... elsif ... else (updated)
by haukex (Archbishop) on Jun 01, 2017 at 08:53 UTC

    Just a quick thought:

    foreach my $i ( $from .. $to ) { next if $i%2 || $i%3 || $i%4 || $i%5; say $i; }

    Works because zero is false and any other number is true (Update: I admit that's a slight simplification, but appropriate in this context. See also Truth and Falsehood).

    Haven't yet given any thought to whether there are any mathematical properties that might make testing all those numbers unnecessary.

    Update: Or even say for grep {!($_%2||$_%3||$_%4||$_%5)} $from .. $to; (although I suspect this might be a bit slower than the above, untested)

    Update 2019-08-17: Updated the link to "Truth and Falsehood".

Re: if ... elsif ... else
by hippo (Archbishop) on Jun 01, 2017 at 08:59 UTC

    For completeness, here is a programmatic way:

    OUTER: foreach my $i ( $from .. $to ) { for (2 .. 5) { next OUTER if $i % $_; } say $i; }

      I would use a statement modifier here

      OUTER: foreach my $i ( $from .. $to ) { $i % $_ and next OUTER for 2 .. 5; say $i; }

      because it is more like natural language, and because I dislike both one-line-xxx(){...}-loops and multiline loops with just one statement in the loop body.

      But that's just a matter of taste or style ;-)

      perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'
Re: if ... elsif ... else
by Eily (Monsignor) on Jun 01, 2017 at 14:00 UTC

    With List::Util, you can write either say $i unless any { $i % $_ } (2,3,5); or say $i if all { $i % $_ == 0 } (3,4,5);

      Please note that  any() all() none() etc. are not found in early versions of List::Util. These functions were introduced first in List::MoreUtils and later included in the former module — but they're still present in the latter.


      Give a man a fish:  <%-{-{-{-<

        I did think about checking if they where present in List::Util or not, but didn't think about previous versions. ++ for the precision.

        Anyway, this is a case where any() can be replaced by grep to obtain the same result. So say $i unless grep { $i % $_ } (2,3,5);

Re: if ... elsif ... else
by kcott (Archbishop) on Jun 02, 2017 at 08:00 UTC

    I'll address optimisation first. If your $from .. $to range is only some hundreds or even a few thousand, then it's probably not worth even looking at optimising. If the range is orders of magnitude larger, or if the loop shown is an inner loop of a larger construct, or if it's part of a subroutine called in another loop, then optimisation may be worthwhile.

    If you do decide to optimise, use the builtin Benchmark module. Run your benchmarks several times — I typically use a minimum of five runs — and throw away outlier results. If it's not absolutely clear that one method is substantially faster than another, assume that there's no optimisation. It's generally better to start out without preconceived ideas about which method will be quicker: that way, you can take the results on face value; won't be disappointed that your pre-benchmark guess was wrong; and won't be tempted to make false assumptions about the results.

    On to condensing the code. Succinct code can be easier to read simply because there's less to read and understand; however, avoid falling into the trap of being too clever and producing a maintenance nightmare. Jamming lots of code into a small space to save a few lines is rarely useful: it makes no difference to Perl, so you get no benefits there; it does, however, make a huge difference to humans — code that's hard to read is hard to understand and maintain — that's true for first-time viewers as well as the original author revisiting after some weeks or months.

    I'd generally avoid writing code like that posted; however, if I did need to, I'd probably aim to lay it out in some tabular format, like:

    if (CONDITION) { ACTION } elsif (CONDITION) { ACTION } ... elsif (CONDITION) { ACTION } else { ACTION }

    I was able, without resorting to any cleverness, to reduce your code to this single statement:

    $_ % 2 or $_ % 3 or $_ % 4 or $_ % 5 or say for $from .. $to;

    I'll run through each step, from your seven lines to my one, as that may help you to do the same with other code. I used the range 0 .. 120 throughout, purely for test purposes. Here's your original:

    $ perl -wE 'foreach my $i ( 0 .. 120 ) { if ($i % 2 != 0) { next; } el +sif ($i % 3 != 0) {next; } elsif ($i % 4 != 0) {next; } elsif ($i % 5 + != 0) {next; } else { say $i;} }' 0 60 120

    If "$i % N != 0" is TRUE, then "$i % N" must be TRUE, so we can remove the " != 0" part throughout:

    $ perl -wE 'foreach my $i ( 0 .. 120 ) { if ($i % 2) { next; } elsif ( +$i % 3) {next; } elsif ($i % 4) {next; } elsif ($i % 5) {next; } else + { say $i;} }' 0 60 120

    Instead of having a next statement for every condition, we can use a single next statement with a multiple condition (cf. your first two responses[1,2]):

    $ perl -wE 'foreach my $i ( 0 .. 120 ) { next if $i % 2 or $i % 3 or $ +i % 4 or $i % 5; say $i; }' 0 60 120

    "next if CONDITION; say $i;" is the same as "say $i unless CONDITION;", so we can get rid of next altogether and reduce the code in the loop to a single statement:

    $ perl -wE 'foreach my $i ( 0 .. 120 ) { say $i unless $i % 2 or $i % +3 or $i % 4 or $i % 5; }' 0 60 120

    foreach and for are synonymous and $_ will be used if you don't supply a variable:

    $ perl -wE 'for ( 0 .. 120 ) { say unless $_ % 2 or $_ % 3 or $_ % 4 o +r $_ % 5; }' 0 60 120

    The "unless CONDITION" part is a Statement Modifier. As it says in the documentation, in emphasised, uppercase text, we can only use a "SINGLE" modifier. Unfortunately, "for LIST" is also a statement modifier, and we want that in the final result. Getting rid of unless is easy:

    $ perl -wE 'for ( 0 .. 120 ) { $_ % 2 or $_ % 3 or $_ % 4 or $_ % 5 or + say; }' 0 60 120

    Now we can use "for LIST" as a modifier, reducing the block to a single statement:

    $ perl -wE '$_ % 2 or $_ % 3 or $_ % 4 or $_ % 5 or say for 0 .. 120' 0 60 120

    — Ken

    A reply falls below the community's threshold of quality. You may see it by logging in.
Re: if ... elsif ... else
by Laurent_R (Canon) on Jun 01, 2017 at 22:13 UTC
    TIMTOWTDI. This is a one-liner solution:

    perl -E ' say for grep { not $_ % 2 | $_ % 3 | $_ % 4 | $_ % 5 } 1..10 +0' 60
    But notice that checking divisibility by 2 and by 4 is redundant-- any number divisible by 4 will also be divisible by 2. And if you think more about it: if you want to keep numbers that are divisible by 2 and divisible by 3, then you can just keep those that are divisible by 6. And similarly for the others. So you can write:
    $ perl -E ' say for grep { not $_ % 60 } 1..200' 60 120 180
    Or, if you don't like grep and want a for loop:
    for my $i ( $from .. $to ) { say $i unless $i % 60; }
Re: if ... elsif ... else
by thanos1983 (Parson) on Jun 01, 2017 at 09:20 UTC

    Hello Anonymous Monk,

    Have you considered also the possibility of Switch? (Not Recommended)

    #!usr/bin/perl use say; use strict; use warnings; # use Benchmark qw(:all) ; # WindowsOS use Benchmark::Forking qw( timethese cmpthese ); sub while_loop { my (@array) = @_; while (defined(my $i = shift @array)){ # This destroys @array, tho +ugh next if ($i % 2 || $i % 3 || $i % 4 || $i % 5); } } sub for_loop { my (@array) = @_; foreach my $i ( @array ) { # This does not destroy the @array next if ($i % 2 || $i % 3 || $i % 4 || $i % 5); } } my $results = timethese(100000000, { For => for_loop( 1 .. 100 ), While => while_loop( 1 .. 100 ) }, 'none'); cmpthese( $results ); __END__ $ perl test.pl (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) Rate For While For 434782609/s -- -83% While 2500000000/s 475% --

    Update: Not a good idea as the monks pointed out. Not recommended it.

    Update2: You could also change your for loop to a while loop it is always faster see code example above.

    Hope this helps.

    Seeking for Perl wisdom...on the process of learning...not there...yet!

      Please do not recommend Switch. The module is a source filter and thus introduces subtle problems if the author of a script using Switch is not careful.

      A source filter is certainly not suitable for beginners in programming who are fighting errors of their own making, as it introduces more errors that have no obvious correlation to the source code.

      Please do not recommend use of the Switch module. It has been long deprecated and has been removed from modern perls. See Re: Switch Module on Perl 5.8.0 for some details of the subject and more links.

A reply falls below the community's threshold of quality. You may see it by logging in.