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
Post a Comment