Reconsidering Exercise 1

Hao Wu provided a great comment showing how I could solve exercise one using sum from List::Util and grep. I'd considered sum but utilzed false laziness and didn't use it. I'd also considered grep, but did not immediately hit upon the elegant solution that Hao suggested and so went with a more verbose solution.

So, here are some new solutions.

Perl 5:

use v5.14;
use List::Util qw(sum);
my $max = (shift || 1000) - 1;

# For each number, collect it if it is a multiple of 3 or 5 (where % returns false)
my @multiples = grep { not $_ % 5 && $_ % 3 } 1 .. $max;

# and we're done.
say "My multiples are: @multiples";
say "Total: ", sum @multiples;

Short, elegant, more obviously correct. What's not to love?

Perl 5i:

use perl5i::2;

my $max = (shift || 1000) - 1;

# For each number, collect it if it is a multiple of 3 or 5 (where % returns false)
my @multiples = grep { not $_ % 5 && $_ % 3 } 1 .. $max;

# and we're done.
say "My multiiples are: @multiples";
say "Total: ", @multiples->sum;

Essentially idential. I could have used grep as a function on the list 1 .. $max but it's more to ugly give grep the subref it requires in that format.

Perl 6:

use v6;

sub MAIN( $max = 1000 ) {

    # For each number, collect it if it is a multiple of 3 or 5 (where %% returns true)
    my @multiples = grep { $_ %% 5 || $_ %% 3 }, 1 .. $max-1; 

    # and we're done.
    "My multiples are: {@multiples}".say;
    say "Total: ", [+] @multiples;
}

I thought that maybe a gather/take would be better than the grep here, but I'm pretty sure that's mistaken. I do like %%, that's pretty neat. "{@array}" threw me for a bit, but I did like [+] for reduce.

And approximate time comparison:

jarich@blackberry:~/polyglot$ time perl m2.pl 30
My multiples are: 3 5 6 9 10 12 15 18 20 21 24 25 27
Total: 195

real      0m0.050s
user      0m0.032s
sys       0m0.016s

jarich@blackberry:~/polyglot$ time perl5i m2.p5i 30 
My multiples are: 3 5 6 9 10 12 15 18 20 21 24 25 27
Total: 195

real      0m0.671s
user      0m0.624s
sys       0m0.040s

jarich@blackberry:~/polyglot$ time perl6 m2.p6 30
My multiples are: 3 5 6 9 10 12 15 18 20 21 24 25 27
Total: 195

real      0m8.731s
user      0m8.561s
sys       0m0.116s

10 Comments

Recent versions of List::Util include a sum0 function which returns a nicer result when, say, $max is 2.

A PDL response might be:

perl -MPDL -E '$x=sequence(999)+1; $x->where( !($x%3) | !($x%5) )->sum'

Plain old Perl looks pretty good here:


$max = (shift || 1000) - 1;

for (1..$max) {
$sum += $_ unless $_ % 5 || $_ % 3
}
print "Total: $sum\n";

This little baby is about two orders of magnitude faster than any of the previously posted solutions...

use strict;
use warnings;
 
my $max = (shift || 1000) - 1;
my $sum = (
   (int($max /  3) * int(($max +  3) /  3) *  3) +
   (int($max /  5) * int(($max +  5) /  5) *  5) -
   (int($max / 15) * int(($max + 15) / 15) * 15)
) / 2;
 
print "Total: $sum\n";

Ain´t that wrong because of short-circuiting?

I´d say you could write it as


for (1..$max) {
$sum += $_ if( $_ % 5 == 0 || $_ % 3 == 0 )
}

This might make it slightly easier to see what's going on in my solution:

use List::Util qw(sum0);
my $sum = sum0 map {
	my $n = int($max/abs $_);
	$n * ($n+1) * $_/2
} qw/ 3 5 -15 /;

We find the number $n of multiples of 3 below $max. Then do the triangular number thing ($n * ($n+1) / 2) and multiply that by 3 to scale it up correctly. Then we do the same for 5. Then, because we've double counted multiples of 15, we do the same for 15 and subtract it.

Overall this solution is O(1) rather than O(n) because we don't need to loop through an increasingly long list of numbers. We can calculate $max=10_000_000 in roughly the same time it takes to calculate $max=10.

What version of Perl 6 are you using for your local timings?

Just did the Perl 6 example on my Macbook Air and got a very different time! I have a feeling the poster is using the JVM build without understanding that it has about ~8 seconds start up time regardless of what you do, even something like `perl6 -e ''`

$ perl6 --version
This is perl6 version 2013.09 built on parrot 5.5.0 revision 0

$ sysctl -a | grep machdep.cpu.brand.string
machdep.cpu.brand_string: Intel(R) Core(TM) i5-2557M CPU @ 1.70GHz

$ time perl6 stuff.p6
My multiples are: 3 5 6 9 10 12 15 18 20 21 24 25 27 30 33 35 36 ... *snip* ... 993 995 996 999
Total: 233168

real 0m1.289s
user 0m1.037s
sys 0m0.220s

My times for the Perl 5 version for comparison:

real 0m0.064s
user 0m0.010s
sys 0m0.015s

So my Mac has a slower time for the Perl 5 code so I'd imagine the OP would have a slightly better time than 1s for the Perl 6 code running in the latest R* parrot build?

Do note that you can use @*ARGS instead of providing a sub MAIN; though the latter will give you a usage message when you don't supply the max and if you write

sub MAIN( Int $max = 1000 )
instead, you'll even get a helpful error message if the user writes "over nine thousand" instead of a number.

Also, for interpolating arrays into strings, you can also just write

"My multiples are: @multiples[]".say;

And another thing: I think "1 ..^ $max" looks prettier than "1 .. $max-1".

Leave a comment

About jarich

user-pic I know Perl 5, but I could stand to know more about perl5i and perl6, so I'm going to try to solve basic to hard problems in each of the three languages, learning the idioms as I go.