Oh oh oh! Finally it’s that time of the year again!
I love Advent of Code and I’ve played it with friends and colleagues for years. Usually, I try to solve it in Ruby, since I’m very familiar with it, and so the effort is only on task solving rather than on learning the language and tooling.
But this year, I thought I might do it both in Ruby and in Raku (The Language Formerly Known As Perl 6), by first solving it in Ruby, and then if I have time trying to solve it again in Raku.
So, here’s a quick view of a couple solutions to the problems in Day 1. Stick to the end for the cool solution past the initial line noise 🙂
SPOILERS AHEAD
The first problem is basically: given a list of values, count the number of times they increase
In Ruby 3, this could look like this
def solve1(array)
array. # [1, 2, 5, 4]
.each_cons(2). # [(1, 2), (2, 5), (5, 4)]
.count { _1 < _2 }. # 2
end
AKA: get a sliding window of pairs from the initial array, then count the occurrences where the first value in the pair is less than the second value in the pair. Notice the fancy anonymous positional block arguments!
You can do more or less the same in Raku, but I’m not aware of a .count
method that takes a block§, but well, we can still do this in three steps:
- build the sliding window
- select the items matching the condition
- get the size of the filtered list
Raku does not have each_slice
, instead it has a .rotor
method which is a generalization of “iterate and group“, you can define in one go both the size of the group and the offset, i.e.
> my @ints = [1, 2, 3, 4, 5, 6, 7, 8, 9]
[1 2 3 4 5 6 7 8 9]
> @ints.rotor(2) # group by 2
((1 2) (3 4) (5 6) (7 8))
> @ints.rotor(2 => 1) # group by 2, and move by one after
((1 2) (4 5) (7 8))
> @ints.rotor(2 => -1) # group by 2, and move back by one after
((1 2) (2 3) (3 4) (4 5) (5 6) (6 7) (7 8) (8 9))
The actual signature of this method is even more flexible, but this should be enough for us.
To filter we can use .grep
, which takes a block. We can do this javacript-like, passing a block with a pointed arrow.§
Then we get the size of the list, which is done with prefix '+'
. This is an odd perlism, just accept it.
+ @ints.rotor(2 => -1).grep(-> ($a, $b) { $a < $b })
So, that should work. But at this point it’s just un uglier Ruby! We can do better!
To get there, let’s first see a different way we can build the list of pairs: we can slice the list and merge it with itself, an operation which most languages call zip
.
Raku also calls it zip, but it has something cooler than a function: it has a zip metaoperator! §
A metaoperator is basically an operator which takes another operator as argument, and augments it’s behaviour.
The simplest I can think of is the reduce metaoperator. Suppose you want to get the product of all items in a list. You know the operation you want, which is *
, and in Ruby, Python, Smalltalk etc.. you would call a method or function called reduce
or inject
passing a function to it.
In Raku, you apply the prefix reduce metaoperator ([]
) to the binary product operator (*
):
> [*] [1,2,3]
6
The Zip metaoperator is the same:, it takes a binary operator and returns a binary operator, which applies its argument to items from both sides zipping them up:
> [1, 2, 3] Z* [10, 20, 30]
(10 40 90)
See where we’re going? Yep, we can apply the first metaoperator to the result of the second!§
> [Z*] [1, 2, 3], [10, 20, 30]
(10 40 90)
So, if we want to know “is the item in the left list larger than the corresponding in the right list?” we can just choose the right operator
> [Z>] [1, 2, 3, 1000], [10, 20, 30, 0]
(False False False True)
This means we can apply it to the slices of the original input to obtain a list of Booleans. Then we just have to count them, and you can do this easily by knowing that in Raku you can add Booleans, and they will be treated as 1 and 0.
Do you know how to add up all items in a list? Yep, reduce metaoperator and binary plus (+
)!
So the full solution is
[+] [Z<] @ints, @ints[1..*];
Or in even shorter form
[+] [Z<] @ints[*,1..*]
This is at a glance impossible to understand, but it’s actually pretty expressive, and I guess it should get easier with habit, we’ll see 🙂
See you another day, happy hacking!
PS
Problem 2 left as an exercise for the reader. You may want to invest time in learning how to use hyper operators, they are cool!