Advent of Code Day 8!
published 2024-12-11
unnecessarily overloading many operators
For Day 7, I want to showcase Raku’s very powerful operator overloading.
(Previously: Day 1, Day 2, Day 3, Day 4, Day 5, in Raku, and Day 6, Day 7, in Elixir).
This is today’s puzzle: Advent of Code description.
First, let’s define a Loc
class:
class Loc {
has Int $.x;
has Int $.y;
method new($x, $y) { self.bless(:$x, :$y) }
method gist { "loc($!x, $!y)" }
}
Pretty straightforward: It holds two public members ($.
is for public member and $!
is for private). self.bless
just creates the class. gist
lets us print this class with say
.
If we wanted to create a Loc
object, we’d need to write Loc.new(3, 4)
. This is fine, but not fun.
I want to instead write {{3, 4}}
. Thankfully, Raku lets us define a wide array of custom operators. Let’s define a circumfix operator to create a Loc
:
sub circumfix:< {{ }} >(($x, $y)) { Loc.new($x, $y) }
To define operators in Raku, we define a function with a particular name (and arguments). Here, we define the function circumfix:< {{ }} >
. It takes in a 2-element list1, and gives us a new Loc
object.
This isn’t very useful, though. Let’s define more operators to make expressing operations on Loc
more natural.
First, let’s define what the negation of Loc means:
multi sub prefix:< - > (Loc $a) { {{-$a.x, -$a.y}} }
Here, we define a multi sub
, which tells Raku to dynamically dispatch based on the arguments passed to the function (we need to do this for prefix:< - >
because the same function is also defined for numbers, for example).
Next let’s define addition and subtraction (notice that subtraction is defined in terms of the addition and negation operators just defined!):
multi sub infix:< + > (Loc $a, Loc $b) { {{$a.x + $b.x, $a.y + $b.y}} }
multi sub infix:< - > (Loc $a, Loc $b) { $a + -$b }
And then the comparison operators:
multi sub infix:< \< > (Loc $a, Loc $b) { $a.x < $b.x and $a.y < $b.y }
multi sub infix:< \> > (Loc $a, Loc $b) { $b < $a }
multi sub infix:< \<= >(Loc $a, Loc $b) { $a.x <= $b.x and $a.y <= $b.y }
multi sub infix:< \>= >(Loc $a, Loc $b) { $b <= $a }
multi sub infix:< == > (Loc $a, Loc $b) { $a.x == $b.x and $a.y == $b.y }
(This doesn’t look as nice because we need to escape the <
and >
in the function definition.)
Finally, for part 2, defining a scaling operator will be useful:
multi sub infix:< * > (Int $s, Loc $a) { {{$s * $a.x, $s * $a.y}} }
Were all these operators necessary? No. They’ll make the solution fun to write, though!
So, onto the actual solution. We’ll read in the file, find the number of rows and columns in the grid, then define a function to tell us if a location is within the grid:
my $file = 'data.txt'.IO.slurp;
my $rows = $file.lines.elems;
my $cols = $file.lines[0].comb.elems;
sub in_bounds(Loc $a) {
$a >= {{0, 0}} and $a < {{$rows, $cols}}
}
Next, we’ll read in the file, storing the locations of each type of antenna separately and adding all antennas to a list of antinodes:
my %antennas;
my @antinodes;
for $file.lines.kv -> $row, $line {
for $line.comb.kv -> $col, $c {
next if $c === ".";
%antennas{$c}.push: {{$row, $col}};
@antinodes.push: {{$row, $col}};
}
}
Next, we’ll loop over the locations of each type of antennas. Raku’s iterable types have a combinations
method that does what it says: find all the combinations.
For each combination of antennas, we’ll calculate the displacement vector between them, then repeatedly add that distance vector in both directions until we’re out of bounds, adding each location to the list of antinodes.
for %antennas.values -> @locs {
for @locs.combinations(2) -> ($a, $b) {
my $vec = $a - $b;
my &generate = -> $p, $i {
my $loc = $p + $i * $vec;
last unless in_bounds($loc);
$loc
};
@antinodes.append: (1, 2 ... *).map(-> $i { generate($a, $i) }).eager;
@antinodes.append: (-1, -2 ... *).map(-> $i { generate($a, $i) }).eager;
}
}
Syntax note: Raku ranges are pretty nice. (1, 2 ... *)
generates an infinite lazy list of 1 to infinity, whereas (-1, -2 ... *)
generates an infinite lazy list of -1 to negative infinity. Because these are lazy, we’ll have to make them eager before appending to the antinodes list (note that the last
in generate
makes sure that we stop evaluating the infinite list so that eager evaluation doesn’t go on forever).
Next, we’ll need to find the unique count of antinodes:
say @antinodes.unique(with => &[==]).elems
We use our custom ==
operator instead of the default strict equality operator ===
(which is only true if left and right arguments are the exact same argument).
I’ll be using Raku one more time, to show off another feature I really like about Raku. It’s more limited in its applicability, so stay tuned!
- like many functional languages, Raku has pattern matching in functions↩