Ved Thiru

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. 1

    In 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 be True.

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.


  1. Apparently, => is known as the “fat arrow” or “hash rocket”.
  2. I recommend opening a Raku REPL (here’s an online one) and trying out some different junctions.
  3. This is basically the same as the Python and Javascript spread operators.
Tags: