matatu blog

Brian writes about computing

Jan 9, 2023

Advent of Code 2022

A Tour of the Raku Programming Language

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 regexes 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;
    

Conclusion

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.