Advent of Code Day 2, in Raku!
published 2024-12-02
what Junctions are, and the return of the Whatever Star
I did Day 2 in Raku again! (Previously: Day 1 in Raku.)
As before, I won’t re-describe the puzzles: take a look on Advent of Code.
Part 1
We are supposed to count the number of safe lines. There are two criteria:
- The levels must be increasing or decreasing;
- the change in levels must be between 1 and 3.
Here’s my solution:
my $file = open 'data.txt';
sub is_safe(@levels) {
my @changes = @levels.rotor(2 => -1)
.map: -> ($a, $b) { $a - $b };
return (*.abs == (1..3).any).(@changes.all)
& ((@changes.all > 0) ^ (@changes.all < 0));
}
my $count = 0;
for $file.lines -> $line {
++$count if is_safe($line.words);
}
say $count;
Let’s take it one step at a time. If you haven’t seen Raku before, or need a refresher, I explain some basics in my Day 1 in Raku post.
Let’s first look at the line in the for loop:
++$count if is_safe($line.words);
This is pretty straightforward: ++
is the preincrement operator, and we only preincrement $count
if is_safe
returns true. Remember that .words
splits a line on whitespace, and so is_safe
takes in a list of strings.
Now let’s look at the first line of is_safe
:
my @changes = @levels.rotor(2 => -1)
.map: -> ($a, $b) { $a - $b };
The goal with this line is to find the change between each level. This is accomplished in two steps:
.rotor(2 => -1)
: The idea here is “turn the list into a list of adjacent pairs”, so that we can find the difference between those pairs..rotor(2)
would give us non-overlapping chunks of numbers. If we run[1, 2, 3, 4].rotor(2)
, we’d get[(1, 2), (3, 4)]
.2 => -1
is a Pair. A Pair is the basic value in a Hash (hence the syntax), but it’s also used in some functions. 1In
rotor
, if you pass a Pair, the first value is the number of elements per chunk. The second is the offset. We offset by -1 to go back 1 from where we would end up. If we run[1, 2, 3, 4].rotor(2 => -1)
, we get[(1, 2), (2, 3), (3, 4)]
. Exactly what we want!.map: -> ($a, $b) { $a - $b }
: Take a look at Day 1 Part 2. This is pretty straightforward: we iterate over the 2-element lists and find the difference between the two components.
Junctions
Junctions are pretty fun. They represent a superposition of values. 2
The easiest way to explain is with an example: 1 == (1..3).any
evaluates to true. First, 1..3
is a Range (think of this as equivalent to [1, 2, 3]
). .any
converts it to a junction of type ”any
”, represented as any(1, 2, 3)
. A Junction of type any
evaluates to True
if any of its components evaluate to True
. 1 == 1
, so the junction becomes any(True, False, False)
. When coerced to a boolean, this evaluates to True
.
There are other kinds of junctions! There’s all
, which is True
if all the components are True
; one
, which is True
if exactly one component is True
; and none
, which is True
if none of the components are True
.
There are operators (of course, because this is Raku) to create these junctions. a&b
is all(a, b)
, a|b
is any(a, b)
, and a^b
is one(a, b)
.
Alright, let’s take a look at the solution again:
return (*.abs == (1..3).any).(@changes.all)
& ((@changes.all > 0) ^ (@changes.all < 0));
*.abs == (1..3).any
: The Whatever Star returns! (Take a look at Day 1 Part 1.) We’re creating a Block (other languages call this a lambda) that checks if the absolute value of the argument is 1, 2, or 3.(*.abs == (1..3).any).(@changes.all)
: We apply this function to@changes.all
. That means that we’re checking if all of the values in the array@changes
are 1, 2, or 3.(@changes.all > 0) ^ (@changes.all < 0)
. The left half checks if all the changes are positive, the right half checks if all are negative, and we join these junctions with^
because only one should beTrue
.
We join with &
because we want both the “check if all changes are 1, 2, or 3” junction and the “check if changes are all positive or negative” junction to be True
.
Part 2
my $file = open 'data.txt';
sub is_safe(@levels) {
my @changes = @levels.rotor(2 => -1)
.map: -> ($a, $b) { $a - $b };
return (*.abs == (1..3).any).(@changes.all)
& ((@changes.all > 0) ^ (@changes.all < 0));
}
my $count = 0;
for $file.lines -> $line {
my @levels = $line.words;
my @levels_pd =
[@levels,
@levels.combinations(@levels.elems - 1).Slip];
++$count if is_safe(@levels_pd.any);
}
say $count;
This is very similar to the previous solution; the highlighted lines have been changed.
@levels.combinations(@levels.elems - 1)
is a list of elements in@levels
with one element removed. The name of the function comes from combinations in mathematics; the order of the elements remains the same as the original list..Slip
turns that list into a Slip, which is “a kind of List that automatically flattens into an outer List”. 3
We also call is_safe
on @levels_pd.any
, to check if any of these options is safe.
That’s all for Day 2. Junctions are a great feature of Raku! Once you use them a few times they become a very natural way to think about checking multiple values.
- Apparently,
=>
is known as the “fat arrow” or “hash rocket”.↩ - I recommend opening a Raku REPL (here’s an online one) and trying out some different junctions.↩
- This is basically the same as the Python and Javascript spread operators.↩