http://qs321.pair.com?node_id=1214667

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

I have a loop that calculates the difference between a variable in an array to the next element in the array. I'm currently using a pretty standard manner of doing this, namely

for (my $i=0;$i<@a;++$i){ $b[$i] = $a[$i] - $a[$i+1]; }

I wondered if there was any way to address the "$_ + 1" thought in a map statement. I've been looking for any examples, but I don't find them. Obviously this isn't it, but that's the thought at least.

@b = map{$_ - $_+1}, @a;

Replies are listed 'Best First'.
Re: Abusing Map
by Eily (Monsignor) on May 16, 2018 at 16:50 UTC

    First, there is an issue with your code, since $i can be the index of the last element, $i+1 can be the index of the element after the last. With warnings on, perl will complain about the use of an undef value.

    Then, there may be a module to do what you want, but one possible way is to use the list of indexes as the input list rather than @a itself. With the range operator .. you can do it like: @b = map { $a[$_] - $a[$_+1] } 0..$#a-1;. Or, to save a few characters: @b = map { $a[$_-1] - $a[$_] } 1..$#a; (with $#array the last index of @array)

Re: Abusing Map (Corrected second code block)
by BrowserUk (Patriarch) on May 16, 2018 at 16:52 UTC

    You could do:

    @b = map{ $a[ $_-1 ] + $a[ $_ ] } 1 .. $#a;

    For small arrays, it doesn't cost too much.

    Update: corrected bounds as pointed out by AnomalousMonk

    But post-fix for is probably better:

    $b[ $_-1 ] = $a[ $_-1 ] + $a[ $_ ] for 1 .. $#a;

    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". The enemy of (IT) success is complexity.
    In the absence of evidence, opinion is indistinguishable from prejudice. Suck that fhit

      Given,

      my @b; $b[ $_-1 ] = $a[ $_-1 ] - $a[ $_ ] for 1 .. $#a;

      Micro-optimized:

      my @b; $b[ $_ ] = $a[ $_ ] - $a[ $_+1 ] for 0 .. $#a-1;

      Micro-optimized further:

      my @b = @a; $b[ $_ ] -= $b[ $_+1 ] for 0 .. $#b-1; pop @b;

      Well, I suspect those are faster. I didn't actually test.

        Well, I suspect those are faster. I didn't actually test.

        Update: Thanks for everyone who's been following along, especially Eily who pointed out the conceptual error with the string evals. Here then is a hopefully correct version (including BrowserUK's optimised "while" solution) using anon subs which has somewhat more believable figures (Perl is fast, just not that fast :-)

        #!/usr/bin/env perl use strict; use warnings; use Benchmark 'cmpthese'; for my $arrsize (1e5, 1e6, 1e7) { print "Source array has $arrsize elements\n"; my @x; push @x, rand for 1..$arrsize; cmpthese (1e1, { BUK => sub { my @y; $y[ $_-1 ] = $x[ $_-1 ] - $x[ $_ ] for 1 . +. $#x; }, ike1 => sub { my @y; $y[ $_ ] = $x[ $_ ] - $x[ $_+1 ] for 0 .. + $#x-1; }, ike2 => sub { my @y = @x; $y[ $_ ] -= $y[ $_+1 ] for 0 .. $#y- +1; pop @y; }, while => sub { my @y; my $i = $#x; $y[ $i ] = $x[ $i ] + $x[ - +-$i ] while $i; }, }); print '-' x 80 . "\n"; }

        Producing this output:

        Source array has 100000 elements (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) Rate BUK ike1 while ike2 BUK 32.3/s -- -19% -32% -32% ike1 40.0/s 24% -- -16% -16% while 47.6/s 48% 19% -- -0% ike2 47.6/s 48% 19% 0% -- ---------------------------------------------------------------------- +---------- Source array has 1000000 elements Rate BUK ike1 ike2 while BUK 2.79/s -- -30% -39% -42% ike1 4.00/s 43% -- -12% -16% ike2 4.57/s 63% 14% -- -5% while 4.78/s 71% 20% 5% -- ---------------------------------------------------------------------- +---------- Source array has 10000000 elements s/iter BUK ike1 ike2 while BUK 3.06 -- -18% -28% -31% ike1 2.49 23% -- -11% -16% ike2 2.21 38% 13% -- -5% while 2.10 46% 19% 5% -- ---------------------------------------------------------------------- +----------

        So the "while" approach does just win out in the end.


        The rest of this post is all cobblers. Upon close inspection I've not set up the arrays correctly. It's the cardinal sin of using @a and @b transliterated from the snippets posted. Mea cupla. See above for the updated script and figures.

        I was intrigued, so I did test.

        #!/usr/bin/env perl use strict; use warnings; use Benchmark 'cmpthese'; my @source; for my $arrsize (1e6, 1e7, 1e8) { print "Source array has $arrsize elements\n"; my @source; push @source, rand for 1..$arrsize; cmpthese (1e8, { 'BUK' => ' my @b; $b[ $_-1 ] = $a[ $_-1 ] - $a[ $_ ] for 1 .. +$#a; ', 'ike1' => ' my @b; $b[ $_ ] = $a[ $_ ] - $a[ $_+1 ] for 0 .. $ +#a-1; ', 'ike2' => ' my @b = @a; $b[ $_ ] -= $b[ $_+1 ] for 0 .. $#b-1; + pop @b; ' }); print '-' x 80 . "\n"; }

        With these results:

        Source array has 1000000 elements Rate ike2 BUK ike1 ike2 2347418/s -- -25% -27% BUK 3126954/s 33% -- -3% ike1 3207184/s 37% 3% -- ---------------------------------------------------------------------- +---------- Source array has 10000000 elements Rate ike2 BUK ike1 ike2 2226180/s -- -28% -31% BUK 3079766/s 38% -- -4% ike1 3204101/s 44% 4% -- ---------------------------------------------------------------------- +---------- Source array has 100000000 elements Rate ike2 ike1 BUK ike2 1886792/s -- -28% -30% ike1 2612330/s 38% -- -3% BUK 2705628/s 43% 4% -- ---------------------------------------------------------------------- +----------

        Here's the proper, working code:

        #!/usr/bin/env perl use strict; use warnings; use Benchmark 'cmpthese'; for my $arrsize (1e4, 1e6, 1e8) { print "Source array has $arrsize elements\n"; my @x; push @x, rand for 1..$arrsize; cmpthese (1e7, { 'BUK' => ' my @y; $y[ $_-1 ] = $x[ $_-1 ] - $x[ $_ ] for 1 .. +$#x; ', 'ike1' => ' my @y; $y[ $_ ] = $x[ $_ ] - $x[ $_+1 ] for 0 .. $ +#x-1; ', 'ike2' => ' my @y = @x; $y[ $_ ] -= $y[ $_+1 ] for 0 .. $#y-1; + pop @y; ' }); print '-' x 80 . "\n"; }

        Giving these figures:

        Source array has 10000 elements Rate ike2 BUK ike1 ike2 2309469/s -- -24% -26% BUK 3030303/s 31% -- -3% ike1 3134796/s 36% 3% -- ---------------------------------------------------------------------- +---------- Source array has 1000000 elements Rate ike2 BUK ike1 ike2 2267574/s -- -28% -30% BUK 3164557/s 40% -- -2% ike1 3236246/s 43% 2% -- ---------------------------------------------------------------------- +---------- Source array has 100000000 elements Rate ike2 BUK ike1 ike2 2277904/s -- -23% -28% BUK 2976190/s 31% -- -7% ike1 3184713/s 40% 7% -- ---------------------------------------------------------------------- +----------

        Oddly, this doesn't afect the qualitative results noticeably. ike2 (with the pop) is still consistently the slowest. The other two are roughly the same to within the detection levels of the test but ike1 may be a smidge faster for large arrays.

Re: Abusing Map
by AnomalousMonk (Archbishop) on May 16, 2018 at 20:41 UTC

    WRT this recent discussion thread: Forget map, if you want to take it up just that extra quantum energy level that allows you to achieve truly epic abuse, use grep:

    c:\@Work\Perl\monks>perl -wMstrict -MData::Dump -le "my @ra = (1, 2, 3, 4); ;; my @rb; grep $rb[ $_-1 ] = $ra[ $_-1 ] + $ra[ $_ ], 1 .. $#ra; dd \@rb; " [3, 5, 7]

    Update: Also works with
        grep $rb[ $_ ] = $ra[ $_ ] + $ra[ $_+1 ], 0 .. $#ra-1;
    with one fewer arithmetic operation per iteration!


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

    A reply falls below the community's threshold of quality. You may see it by logging in.
Re: Abusing Map
by BrowserUk (Patriarch) on May 17, 2018 at 09:40 UTC

    On the other hand, if you would prefer concise and efficient, use:

    my $i = $#a; $b[ $i ] = $a[ $i ] + $a[ --$i ] while $i;

    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". The enemy of (IT) success is complexity.
    In the absence of evidence, opinion is indistinguishable from prejudice. Suck that fhit
      $b[ $i ] = $a[ $i ] + $a[ --$i ] while $i;

      Except that crashes with a "Modification of non-creatable array value attempted ..." fatality (after a couple of warnings) for an empty input array. I've only visually examined them, but all the preceeding for-loop and map (and even grep!) solutions seem as if they would handle empty arrays gracefully. But
          $b[ $i ] = $a[ $i ] + $a[ --$i ] while $i > 0;
      avoids the problem.


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

        fatality (after a couple of warnings) for an empty input array

        So don't do that :)

        Test before entering the loop:

        ( my $i = $#a ) > 0 or die; $b[ $i ] = $a[ $i ] + $a[ --$i ] while $i;

        Never do in a loop, what can be done outside of it.

        Wouldn't your version fail for an array with one element?


        With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority". The enemy of (IT) success is complexity.
        In the absence of evidence, opinion is indistinguishable from prejudice. Suck that fhit

      Doesn't that rely on undefined behaviour though? Auto increment and Auto decrement:

      Note that just as in C, Perl doesn't define when the variable is incremented or decremented. You just know it will be done sometime before or after the value is returned.
      ie: the doc doesn't rule out your code being interpreted as eval { --$i; $b[ $i ] = $a[ $i ] + $a[ $i ] } while $i;

      For example: $b[ $i ] = [ $i, --$i ] while $i; builds a list of identical pairs.</c>

      Correct and efficient might be:

      $#b = $#a-1; # Allocate a big enough array $i = @b; $b[ $i ] = $a[ $i ] - $a[ $i+1 ] while $i--;

        Doesn't that rely on undefined behaviour though? ... ie: the doc doesn't rule out your code being interpreted as ...

        Yep. And in 16 years, the behaviour, 'defined by the implementation', hasn't changed; and in the remotely possible circumstance it does, I'll fix it then.

        (Assuming any of my code ever gets run on anything newer than 5.16; and then probably by moving it to Julia.)


        With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority". The enemy of (IT) success is complexity.
        In the absence of evidence, opinion is indistinguishable from prejudice. Suck that fhit
        div class=
Re: Abusing Map
by LanX (Saint) on May 17, 2018 at 03:39 UTC
      I'm surprised nobody mentioned reduce yet.

      Because that doesn't work to produce the results the op wants.

      So mentioning it would be silly.


      With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority". The enemy of (IT) success is complexity.
      In the absence of evidence, opinion is indistinguishable from prejudice. Suck that fhit
      use List::MoreUtils qw( zip6 ); my @b = map { $_->[0] - $_->[1] } zip6 @{[ @a[0..$#a-1] ]}, @{[ @a[1..$#a] ]};

      :)