Advent of Code 2022, but in JS and point-free style: Day 1

Piotr Jaworski
6 min readDec 6, 2022

--

Photo by Elena Mozhvilo on Unsplash

The complete list of articles up to date:

Being a massive fan of the functional programming paradigm and using it wherever possible, I’ve decided to attempt to tackle this year’s Advent of Code challenge with nothing but point-free style code and (if possible) using only Ramda library functions. Below is my attempt at Day 1!

First, of course, we need to import Ramda (assuming it’s already installed):

import R from "ramda";

Well, simple enough. Next, for the challenge input. For simplicity, I’m keeping the input as a txt file next to my index.js , so I can import it like this:

import R from "ramda";

import data from "./data.txt";

The input is identical to the source, so it’s a string containing numerical values delimited by new lines and, sometimes, by empty ones.

Let’s take a look at the task at hand. Our data is a list of foods carried by Elves. Each line represents the calorific value of a given piece of food, and an empty line delimits food carried by each Elf. We need to calculate which Elf carries the food with the most calories in total. So, we need to sum all the values per each Elf.

First, let’s create a pipeline for the functions we will be using. Since we’re going to output the result somewhere, I’m going to add the last one (which will be a console.log call) first — this way, it will help us see intermediate results as we go:

import R from "ramda";

import data from "./data.txt";

R.pipe(
console.log
)(data);

The result of this is, of course, a very, very long multi-line string. Let’s break it into an array containing values of individual lines:

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
console.log
)(data);

This is pretty straightforward. We end up with a 2235-element-long array of strings, each representing a single line in the original input file. Since we will be summing those, let’s parse them into actual numbers.

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
R.map(parseInt),
console.log
)(data);

At this point, it’s worth noting that our empty lines (which help us identify the Elves) got parsed into NaNs. It is completely fine, as we love Indian cuisine! It will also still be helpful to identify where one Elf ends and another begins:

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
R.map(parseInt),
R.groupWith(R.identity),
console.log
)(data);

OK, let’s slow down here. The complexity of this line is a bit higher than the previous ones. groupWith (docs) will help us split our array into chunks, each representing one single Elf and the calorie burden they’re carrying. It accepts a comparison function, which is called with two arguments — the current and the next element of the iterated list. If the comparison function returns a truthy value, the current element is added to the existing chunk. For a falsy value, a new chunk is created and populated with the current element.

In our case, we want to start a new chunk at the border between two Elves, which is currently represented by a NaN (for those interested, an average Naan bread has, according to google.com, 262 calories). We can use the fact that NaN (despite its caloric value) is a falsy value. All we need is a comparison function that will take the first argument and return it. We can use the identity function — all it does is returns the passed argument, which, in our case, will be coerced to a Boolean.

OK, so now we have an array of arrays (each for one Elf), but there’s a problem — groupWith chunks include all the elements of the original array. We need to get rid of the NaNs (I won’t ride on that Naan pun anymore):

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
R.map(parseInt),
R.groupWith(R.identity),
R.map(R.filter(R.identity)),
console.log
)(data);

In this step, we map all our Elves by filtering their arrays by excluding NaNs. For this, all we need to do is pass the identity function to the filter one, just as in the previous step.

Now that we have a nice, clean list of calories (represented as numbers) for each Elf, we can proceed to sum them:

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
R.map(parseInt),
R.groupWith(R.identity),
R.map(R.filter(R.identity)),
R.map(R.sum),
console.log
)(data);

We could use a reduce call as an argument to the map, but luckily, there’s no need — Ramda provides a useful sum function.

We now have a list of calories carried by each Elf! The last thing to do is find the one that carries the most:

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n"),
R.map(parseInt),
R.groupWith(R.identity),
R.map(R.filter(R.identity)),
R.map(R.sum),
R.apply(Math.max),
console.log
)(data);

Why do we need to use apply here? The previous piped function returns an array of numbers and Math.max accepts single numbers as multiple arguments. apply does exactly that — calls the supplied function on the provided list of arguments as spread ones.

The code above not only provides a correct answer to Day 1 of 2022's Advent of Code but also is (in my opinion) quite elegant and, what’s more, looks a bit like a Christmas Tree!

Edit: I was so excited about helping the Elves carry the maximum amount of calories that, apparently, I missed the second part of the task! I’d like to use this opportunity to explain another cool concept. Here’s the extended solution:

import R from "ramda";

import data from "./data.txt";

const inversedSubtract = R.flip(R.uncurryN(2, R.subtract));

R.pipe(
R.split("\n"),
R.map(parseInt),
R.groupWith(R.identity),
R.map(R.filter(R.identity)),
R.map(R.sum),
R.sort(inversedSubtract),
R.tap(R.pipe(R.head, console.log)),
R.tap(R.pipe(R.take(3), R.sum, console.log))
)(data);

An observant reader will spot that there are a couple of differences. Let’s start from the top. First, we added a inversedSubtract function — the inversion is needed to, well, inverse the order of arguments without needing to use a lambda function. To achieve that, we need to call two Ramda functions — uncurryN uncurries a function so it can be flipped and, well, flip. The only thing that’s complicated here is the need to provide the arity of the function as a first argument to uncurryN — in this case, it’s 2 (which is the arity of the subtract function).

A genuinely engaged reader will also notice that the code in the main pipeline changed at some point. The last line shared with the previous solution is that in which we calculated a sum of calories for each individual Elf using R.map(R.sum). We can use invertedSubtract immediately after that line, by sorting the whole array in descending order.

Why would we need to do that? Well, now we need to calculate two values from one data flow, and the sorted array is actually where our future calculations fork. What do I mean by that? It’s possible to tap into the value and use it for some transformations while allowing it to be passed further down the pipeline. How is that achieved? It shouldn’t be a surprise that we need to use the tap function. The function basically accepts one argument, which is a function that will be run on the value as it is in the current place of the pipeline. At the same time, the value is passed further down the pipeline to be used for other transformations. If we pass a pipe function to the tap one, we basically create a separate pipeline inside a pipeline.

Let’s see how that works. We call

R.tap(R.pipe(R.head, console.log))

immediately after sorting each Elves’ calorie count descending. If we call the function above on that sorted array, we can extract the top Elf’s score (using head) and log it while the array is also passed into the next line:

R.tap(R.pipe(R.take(3), R.sum, console.log))

in which we take the three most calorie-carrying Elves and sum up their cargo — which is the goal of the second half of the first task!

A working example can be found in this CodeSandbox. Follow me for articles covering the next days! In case of questions — feel free to ask them in the comments.

--

--

No responses yet