The 2022 advent of code had a nice variety of problems which provided an excuse to use some of the unique features of the Raku programming language. Below are a number of my Raku solutions, with some explanations. This collection of problems had some common themes, so I'm putting them together here. I might write up another collection in a future post.
Note that to see the problems and the inputs, you'll need to follow the links below. I am only including the solutions, and commentary about how they work. Some of what follows might not make sense if you haven't read the problems, or perhaps tried to solve them yourself.
Day 1: Calorie Counting
In this problem we want to find the sums of a sequence of lists, then the maximum value, and then the top three values.
To read the input, we use $*ARGFILES
which refers to files
given on the command line. i.e. To run this program, save it
as day-01.raku
, save the input as input.txt
, and then
type raku day-01.raku input.txt
.
The map
is a method call, and *
creates an anonymous
function. Method calls can use either colons or parentheses.
There are other ways to make anonymous functions -- for instance,
each of these lines does exactly the same thing:
.map: *.lines .map( *.lines ) .map( { .lines } ) .map( { $_.lines } ) .map( { $^l.lines } ) .map(-> $l { $l.lines } )
The lines are coerced to numeric values when they are used by sum.
my $input = $*ARGFILES.slurp;
my $elves = $input.split("\n\n");
my @totals = $elves.map: *.lines.sum;
# part 1
put @totals.max;
# part 2
put @totals.sort.tail(3).sum;
Day 3: Rucksack Reorganization
So we have a list of strings of letters, and we want to find 1) the letters that the first and second half of each list has in common, and 2) the letters that groups of three of the strings have in common. Then we want to sum values corresponding to those letters.
We make %vals
to map the letters to their values. Note that Z
stands
for "zip" -- it combines two lists. It takes an operator as a parameter,
so by giving it the pair construction operator, =>
, we construct a list
of pairs. This becomes a hash when we store it in %vals
. Z
is called
a "metaoperator" because it is an operator that uses another operator.
Another metaoperator is []
-- also known as reduce
. This applies
a binary operator to consecutive elements of a list.
For instance, [+] (a,b,c,d)
is a + b + c + d
.
When we use set intersection, ∩
with the meta reduction operator, we are taking
the intersection of a list of lists. So for part one, [∩]
is the common elements of the
first and second half of the list, and for part two, [∩]
is the intersection
of three lists.
Also, comb divides a string into characters, and rotor takes elements of a list several at a time.
By the way, ∩
can be written as (&)
: all of Raku's unicode operators
have ASCII equivalents.
my $in = $*ARGFILES.slurp;
my %vals = ('a'..'z','A'..'Z').flat Z=> 1..52;
# part 1
say sum $in.lines.map: -> $backpack {
%vals{ [∩] $backpack.comb[0 .. */2 - 1, */2 .. * ] }
}
# part 2
say sum $in.lines.rotor(3).map: -> $group {
%vals{ [∩] $group.map(*.comb) }
}
Day 6: Tuning Trouble
We want to find the first time that four consecutive characters are all different. Fourteen for part two.
We comb
the string into a sequence
of letters, and convert the sequence into a list.
Then -- remember rotor above? -- well, it can take
a Pair
as an argument to return a list of lists with overlaps (that is 1,2,3
, 2,3,4
etc).
That's handy for this problem!
Also first searches for an element of a list.
And when we give it the adverb :k
, we get the index, not the element. This is useful for a few
problems this year.
my $in = $*ARGFILES.slurp.comb.list;
# part 1
say 4 + $in.rotor(4 => -3).first: :k, *.unique.elems == 4;
# part 2
say 14 + $in.rotor(14 => -13).first: :k, *.unique.elems==14
Day 7: No Space Left on Device
We are examining the output of various cd
and ls
commands to compute
the sizes of directories on a filesystem, and find the directories using the most space (part 1)
and the minimal set of directories to remove to free up some space (part 2).
There are a few interesting things here:
First, we can make regex
es which are building blocks for the matches in the when
statements. The names of the regexes can then be used in the corresponding statements.
That is, we make a regex named dir
, and then after matching it using <dir>
, we can
refer to $<dir>
to get the match. $<dir>
is exactly equivalent to $/<dir>
-- the
special variables $/
contains the results of the match, and it holds the named captures,
like dir
, size
and filename
. One of the nice features of Raku is that it made
regexes composable -- and this is a small example of using that feature.
Also, the :s
in the regexes in the when
clauses means that whitespace is significant:
e.g. the space between cd
and <dir>
will match whitespace in the input.
Second, remember the metareduction operator []
above? Well there is also a version with
a \
called the "triangular" metareduction operator. The
difference is that this one returns the intermediate results:
[+] (a,b,c,d) is a + b + c + d [\+] (a,b,c,d) is (a), (a+b), (a+b+c), (a+b+c+d)
Instead of +
, we can use concatenation, ~
, to get all the parent directories
of a particular subdirectory. ( /a
, /a/b
, /a/b/c
...)
Also »+=»
is another hyper operator -- this applies +=
pairwise between two lists. i.e. perform
vector addition. (It can also be written as >>+=>>
). The list repetition operator, xx
, just
repeats an element to create a list.
my $in = $*ARGFILES.slurp;
my @cwd = '/';
my %totals;
my regex dir { <[a..z]>+ }
my regex size { <[0..9]>+ }
my regex filename { \w+ }
for $in.lines {
when /:s '$' cd '/' / { @cwd = ('/') }
when /:s '$' cd <dir> / { @cwd.push("$<dir>/") }
when /:s '$' cd '..' / { @cwd.pop }
when /:s <size> <filename>/ {
%totals{ [\~] @cwd } »+=» $<size> xx *
}
}
# part 1
say %totals.values.grep( * <= 100_000).sum;
# part 2
my $total-space = 70_000_000;
my $unused = $total-space - %totals{ '/' };
my $need = 30_000_000;
my %choices = %totals.grep: { $unused + .value > $need }
say %choices.values.min;
Day 8: Treetop Tree House
We have a grid of numbers and we want to: (1) find all trees which are visible from outside the grid, and (2) find the most "scenic" spot.
First of all, X
is the cross-product meta-operator. Like Z
(above)
it takes two lists, but instead of corresponding elements it takes all
possible pairings. That is, 1,2 X~ 3,4
is 13,14,23,24
.
And by the way, you can make your own infix operators. So, 🌳
is
an operator here that finds the index of the first element of a list
which is larger than a given value.
I can then use X
to just apply this in all four directions, and take
the product (using [*]
to reduce it), and then keep track of the
maximum (for part 2).
For part one -- the all and
any are junctions.
Junctions are a nice construct since they turn individual values into
a superposition of values -- a simpler example would be 1 == any(1,2,3)
which compares 1 to three other values. These autothread -- meaning
the expression might be evaluated in parallel in different threads at
the same time.
Another kind of nice thing here is being able to take a slice in any
direction of a two dimensional array, like this: @forest[row^..N;col]
.
The range operator, ..
, constructs a Range
with two endpoints, and the variants ^..
and ..^
can be used to excude the left or right endpoint. Also ^5
is a range too --
the numbers from 0 to 4.
Oh also, the expression $in.lines».comb
applies .comb
to each line.
The operator »
is a "hyperoperator" -- it is equivalent to map
,
except that it also might autothread.
You might also notice that some of these variables are declared with \
-- this
allows them to be used without sigils, e.g. N
, height
, row
, and col
.
In cases where it's obvious what kind of a variable something is, it can be
a little cleaner to leave the sigil out, instead of having a $
in front of it
each time.
my $in = $*ARGFILES.slurp;
my @forest = $in.lines».comb;
my \N = @forest.elems - 1;
sub infix:<🌳>(@trees,\height) {
return $_ + 1 with @trees.first: :k, * >= height;
@trees.elems
}
my ($visible, $scenic-score) = (0,-Inf);
for 0..N X 0..N -> (\row, \col) {
my \height = @forest[row;col];
$visible++ if [
@forest[row;^col].all, @forest[row;col^..N].all,
@forest[^row;col].all, @forest[row^..N;col].all
].any < height;
$scenic-score max= [*] [
@forest[row;^col].reverse, @forest[row;col^..N],
@forest[^row;col].reverse, @forest[row^..N;col]
] X🌳 height
}
say $visible; # part 1
say $scenic-score # part 2
Day 13: Distress Signal
For day 13, we are asked to create a new comparison operator. The operator should behave differently depending on whether the operands are integers or lists.
First, to convert a string of brackets and commas into a nested list, we
use from-json
from JSON::Fast
. (Install it with zef install JSON::Fast
)
We use the hyperoperator, »
, twice on the input -- once to turn all the pairs
into two strings, and then once to turn each of the two strings into json. Again,
this operator works just like map
.
This problem is an even better case for making a custom operator -- since the
problem basically describes a new operator. This time we make one
called ◆
, and we can make use of multiple-dispatch.
When we define a function with multi
,
it is a multiple-dispatch candidate; i.e. the calls in the arguments are mapped
to the version with a matching signature. Are we comparing two
integers? Are we comparing a list to an integer? Are we
comparing two lists? The candidates correspond to the possibilities in the
problem description.
We can use this our custom operator recursively, we can use it with Z
, and we
can fall back to the well know space-ship operator <=>
which just compares
two integers.
Finally, when we are flattening our list of pairs, we use the syntax @lines[*;*]
--
this turns a list of lists into a list, then we can append the sentinel values
to this before finding their indices (using first
again).
use JSON::Fast;
my $in = $*ARGFILES.slurp;
my @lines = $in.split("\n\n")».lines».map: &from-json;
# ◆: compare packets
multi infix:<◆>(Int $a, Int $b) { $a <=> $b }
multi infix:<◆>(@a, Int $b) { @a ◆ [ $b ] }
multi infix:<◆>(Int $a, @b) { [ $a ] ◆ @b }
multi infix:<◆>(@a, @b) {
(@a Z◆ @b).first(* != Same) or (@a.elems <=> @b.elems)
}
# part 1
my @less = @lines.grep: :k, { .[0] ◆ .[1] == Less }
say sum @less »+» 1;
# part 2
my @flat = |@lines[*;*], [[2],], [[6],];
my @sorted = @flat.sort: &infix:<◆>;
my $first = 1 + @sorted.first: :k, * eqv [[2],];
my $second = 1 + @sorted.first: :k, * eqv [[6],];
say $first * $second;
This post demonstrated a few of the unique features of the Raku programming language: hyperoperators, metaoperators, custom operators, composable regexes, junctions, and a variety of constructs for making anonymous functions.
Solutions to the other advent of code problems can be found here. A future blog post may have some more explantations about each of those.