Raku: Advent of Code 2020 - Day Six

Here is my final short form Raku answer to the puzzle for Day 6 of Advent of Code:

sub rv (&code) { 'input'.IO.slurp.split("\n\n", :skip-empty).map(&code).sum }
say "One: " ~ rv { .comb(/\S/).Set.elems };
say "Two: " ~ rv { .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems };


Below is the initial Raku code that earned me my gold stars. How did I get from the code below to what's above? Commentary and refactoring notes follow...

#!/usr/bin/env raku
use v6.d;

sub MAIN (
IO() :$input where *.f = $?FILE.IO.sibling('input'),
Int :$part where * == 1|2 = 1, # Solve Part One or Part Two?
--> Nil
) {
given $part {
when 1 {
say $input.slurp
.split("\n\n", :skip-empty)
.map({ my $x = $_; $x ~~ s:g:s/\s+//; $x.comb.Set.elems })
.sum;
}
when 2 {
my $sum=0;
for $input.slurp.split("\n\n", :skip-empty) -> $group {
my $set;
for $group.lines -> $person {
if (! defined($set)) { $set = $person.comb.Set };
$set = $set $person.comb.Set;
}
$sum += $set.elems;
}
say $sum;
}
}
}

In any unfamiliar language it is a bit frustrating when you know what you want to say, but you don't know how to say it. The Raku Advent of Code github repo for solutions provides a great source for examples of different programming styles by different programmers. mienaikage's solutions have been a great inspiration. I usually find myself taking a similar approach to how mienaikage solves the puzzles. But where I struggle to express myself in Raku, their code is clear, elegant, and concise. mienaikage's code is very close to what I wish I had written... If only I'd known it could be done that way.

Refactoring Part One

say $input.slurp
.split("\n\n", :skip-empty)
.map({ my $x = $_; $x ~~ s:g:s/\s+//; $x.comb.Set.elems })
.sum;

Here within the map block I'm bumping up against an immutable string, forcing it into a scalar, removing whitespace, getting all combinations, using a set to ignore duplicates, and finally getting a sum of the elements. When instead, I could have written:

say $input.slurp
.split("\n\n", :skip-empty)
.map({ .comb(/\S/).Set.elems })
.sum;

.comb with a regex matcher parameter returns a Seq of non-overlapping matches. So instead of removing the whitespace, I can simply ignore it, and move directly to getting the sequence of all non-whitespace characters.

If I had cared about preserving order, I could have used .unique instead of creating a set. unique is probably faster. But because I'm using set logic in Part Two, I chose to stay in the set logic headspace instead of using .unique.

Refactoring Part Two

my $sum=0;
for $input.slurp.split("\n\n", :skip-empty) -> $group {
my $set;
for $group.lines -> $person {
if (! defined($set)) { $set = $person.comb.Set };
$set = $set $person.comb.Set;
}
$sum += $set.elems;
}
say $sum;

Here what I really wanted to do, was to get the set intersection of questions answered "yes". But I didn't know how to coerce the right feed of results into a intersection of answers for each person. Thank you mienaikage for showing how to use reduce to accomplish this. With reduce, Part Two can be refactored to:

say $input.slurp
.split("\n\n", :skip-empty)
.map({ .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems })
.sum

Removing code duplication

Now that Parts One and Two have been cleaned up, lets look at the updated code:

#!/usr/bin/env raku
use v6.d;

sub MAIN (
IO() :$input where *.f = $?FILE.IO.sibling('input'),
Int :$part where * == 1|2 = 1, # Solve Part One or Part Two?
--> Nil
) {
given $part {
when 1 {
say $input.slurp
.split("\n\n", :skip-empty)
.map({ .comb(/\S/).Set.elems })
.sum;
}
when 2 {
say $input.slurp
.split("\n\n", :skip-empty)
.map({ .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems })
.sum
}
}
}

We can now see that Parts One and Two are identical except for what goes on in the first {...} map code block.

$input.slurp
.split("\n\n", :skip-empty)
.map({
{...}.elems })
.sum

Only what goes on in {...} is different. So let's take a step back and refactor things so that we aren't writing redundant code. All the duplicated code can be extracted into a routine which takes a code block parameter:

sub result ( &code --> Int ) {
$input.slurp.split("\n\n", :skip-empty).map(&code).sum;
}

This allows us to arrive at:

#!/usr/bin/env raku
use v6.d;

sub MAIN (
IO() :$input where *.f = $?FILE.IO.sibling('input'),
Int :$part where * == 1|2 = 1, # Solve Part One or Part Two?
--> Nil
) {
sub result ( &code --> Int ) {
$input.slurp.split("\n\n", :skip-empty).map(&code).sum;
}

given $part {
when 1 {
say result { .comb(/\S/).Set.elems };
}
when 2 {
say result { .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems };
}
}
}

Final Version

Finally because we only have a single sub MAIN () {...} and the entire body of the program resides within MAIN's code block... we can use the unit declarator. This allows us the remove the braces for MAIN's code block. Saving a little horizontal screen real estate: 

#!/usr/bin/env raku
use v6.d;

unit sub MAIN (
IO() :$input where *.f = $?FILE.IO.sibling('input'),
Int :$part where * == 1|2 = 1, # Solve Part One or Part Two?
--> Nil
);

sub result ( &code --> Int ) {
$input.slurp.split("\n\n", :skip-empty).map(&code).sum;
}

given $part {
when 1 {
say result { .comb(/\S/).Set.elems };
}
when 2 {
say result { .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems };
}
}

Final Final Version (The Short Form Answer)

While the code above is where I would stop for readability, with the boilerplate removed we can be a little more terse:

sub rv (&code) { 'input'.IO.slurp.split("\n\n", :skip-empty).map(&code).sum }
say "One: " ~ rv { .comb(/\S/).Set.elems };
say "Two: " ~ rv { .lines.map({ .comb(/\S/).Set }).reduce(&infix:<>).elems };
 

Comments

Popular posts from this blog

Raku: Setting up Raku (for Contributors)

Raku: Advent of Code 2020 - Day Twelve

Raku: Advent of Code 2020 - Day Thirteen