Advent of Code 2020, preparing

Posted on
aoc haskell

I’m going to try and complete the Advent of Code puzzles this year, in Haskell, without bouncing off of those stupid interpreter problems that have always driven me away in the past. Encouraged by a fellow poster on lobste.rs, I’m going to try and blog my attempt here.

I don’t have a lot of extra time, but I would like to be better at blogging, and also at Haskell, so let’s hope that this works out. I’m hoping to build the habits here over the next couple of weeks that will enable me to solve the puzzles in a timely fashion, and write up a description of my solution. I’ve done a bit of Haskell, largely in this Advent of Code context, so I’m not entirely a beginner, but I’m nothing like a “Haskell programmer” yet.

GETTING STARTED

I use a Macintosh, so I’ll install stack via homebrew

% brew install haskell-stack

… and I’ll create a new project:

% mkdir -p $HOME/Projects/AOC
% cd $HOME/Projects/AOC
% stack new AOC2020
% cd AOC2020
% stack build AOC2020
# a lot of stuff happens ....
% stack exec AOC2020-exe
someFunc

When the stack new AOC2020 command completed, it created a directory called AOC2020, that has a bunch of directories with stuff in them:

% ls -1 */*
app/Main.hs
src/Lib.hs
test/Spec.hs

It sure looks like the meat is here in app/Main.hs:

module Main where

import Lib

main :: IO ()
main = someFunc

This seems to indicate that there’s a Lib module we can import someFunc from, and in fact, if we look at src/Lib.hs, there we go:

module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

WHAT’S THE PROBLEM?

In keeping with a fellow lobste.rs poster, I’ll start with last year’s Day 1. The description of the problem is

[fuel] required to launch a given module is based on its mass. Specifically, to find the fuel required for a module, take its mass, divide by three, round down, and subtract 2.

That seems like a pretty clear formula, so let’s write it in Haskell baby-talk, as a function from mass to fuel. Haskell has this equational style where you basically write the equation of the calculation you want in a very straightforward fashion (none of the ceremony of, say, Java’s public static void main () {} foofaraw), so the basic definition is the legal one:

fuel :: Int -> Int
fuel mass = div mass 3 - 2 

That looks pretty simple! div is integer division, and operator precedence means there are no brackets needed. The line with the :: in it defines the type of the function – in this case, from Int to Int. Let’s write a quick test function that uses the examples given:

For a mass of 12, divide by 3 and round down to get 4, then subtract 2 to get 2.

For a mass of 14, dividing by 3 and rounding down still yields 4, so the fuel required is also 2.

For a mass of 1969, the fuel required is 654.

For a mass of 100756, the fuel required is 33583.

… to see if we got the logic right:

test :: Bool
test = fuel 12 == 2 && fuel 14 == 2 && fuel 1969 == 654 && fuel 100756 == 33583

OK, but where to put all this code?

A DIGRESSION

I’m going to be writing all my code in Emacs – I use the doom distribution (because life is too short to maintain my own pile of elisp), and to doom’s init.el I added (haskell +dante). Now, I can start an “inferior-mode” interpreter and send my code over to it with C-c C-l.

BACK TO THE HASKELL

I’ve decided to start out with one module per day – obviously, this may not be the ideal way to organize everything, but eh, we’ll burn that bridge when we get there. So, let’s create a lib/Day00.hs module, and put our definitions in there:

-- | 

module Day00 where

test :: Bool
test =
  fuel 12 == 2 &&
  fuel 14 == 2 &&
  fuel 1969 == 654 &&
  fuel 100756 == 33583

-- see what we did here? We re-wrote `fuel` in "point-free" style! More on this, later
fuel :: Int -> Int
fuel = max 0 . subtract 2 . flip div 3

Emacs fills out the module Day00 bit, so all I have to do is add the definitions from above. Now, I can load this source into GHCi, with C-c C-l, and run it:

λ> fuel 12
2
λ> test
True

So far, so good! Now, let’s look at the rest of the problem definition.

What is the sum of the fuel requirements for all of the modules on your spacecraft?

Here, we encounter our first real problem: we have a file, with a bunch of numbers, one per line, and we need to sum the result of calling fuel on each of them. We’re going to have to deal with Haskell’s notorious IO system, which could mean learning all about monads and endofunctors and whatnot – or, I could crib like a maniac from Stack Overflow and figure it out later.

I vote we punt. Let’s assume that we have some mechanism of reading a file, and getting the contents of that file into one big String. Let’s solve the problem in a pure fashion with that String as input, and deal with the IO stuff afterwards. Let’s break this problem down: we have a String and we need to turn that into a list of integer values, representing masses; compute the fuel requirement for each mass; and then sum them together. This sounds a lot like three functions:

stringToStrings :: String -> [String]
stringToMass :: String -> Int
finalFuel :: [Int] -> Int

HOOGLE

Surely we cannot be the first people who want to split a String into a [String]! Or convert a String into an Int! But how to find out what functions we have available?

This is where Hoogle comes in. Hoogle is a search engine for Haskell code, and one of the things you can do with it is search by type signature. This is so amazingly cool: you can say, hey, we have a String and we need [String] – and, voila. The very first result is lines :: String -> [String] and the description of the function is:

lines breaks a string up into a list of strings at newline characters. The resulting strings do not contain newlines.

That looks pretty good to me!

How about String -> Int? Hmmm. Nothing here looks reasonable; why is that? Well, it’s because we’re not operating at the right level of generality here. I don’t fully understand how this all is going to work, but I did some Stack Overflowing, and I figured something out. There isn’t a specific String -> Int function; there is instead a function called read that can be used here, with a type coercion/specification (I don’t know the exact lingo here). This has to do with something called “type classes”, which I’m sure I’ll learn more about as we go. The invocation looks like:

stringToMass :: String -> Int
stringToMass s = read s :: Int

We can now start to stitch our functions together:

computeAnswer :: String -> Int
computeAnswer = sum . map (fuel . stringToMass) . lines

Point-free style rears its head again, I see.

REVISITING IO

So we have a function that takes a String representing a list of masses, one per (Unix) line, and computes the total fuel necessary for that mass. But that’s not going to be enough! We also need a way to read this data in from a file, and then apply our computeAnswer to it.

The first thing I’ll do is download the data to use into a location in the AOC2020 project that makes sense:

% mkdir -p $HOME/Projects/AOC/AOC2020/data
# copy the data from the web page ...
% pbpaste > $HOME/Projects/AOC/AOC2020/data/Day00.txt
% head $HOME/Projects/AOC/AOC2020/data/Day00.txt
103450
...

Let’s try this:

part01 = do
  raw <- readFile "data/Day00.txt"
  putStrLn raw

This now reads the contents of data/Day00.txt and outputs it. Not remotely interesting, but now at least we have read some data from disk and have done something with it.

When I open Day00.hs in my Emacs, it complains about missing top-level type annotations for fuel, test, and part01. You may have seen these type annotations in Haskell source:

isPositive :: Int -> Bool

We read this as “isPositive is a function from an Int to a Bool”. We don’t have the definition, but we already know a lot about isPositive.

What are the type signatures for our functions defined above? Well, I can ask Haskell, by navigating to an expression and using C-c C-t to instruct Emacs to ask Haskell for me, or by using the :t operator in GHCi:

λ> :t part01
part01 :: IO ()

So IO () is apparently the type of the “IO Monad”. I don’t particularly care which metaphor we use to describe it; I think of this as a mechanism for the type system to keep impure code formally distinct from pure code, allowing the compiler to do more work for us; and that’s as much about it as I need right now. If I go deeper, I can see that readFile is of type FilePath -> IO String, meaning that that top-level IO is now also part of the type of the data returned from readFile. But how do we operate on an IO String? This is where the do comes in, I think? We are operating in an sort of “IO context”, so in that context, I can treat an IO String as a String? Is that right? It’s right enough for me, for now.

The basic String type in Haskell apparently is just an alias for a list of Char; in other words, [Char]. So anywhere we see String, we can think [Char], and vice versa. Looking at this again, we can see that readFile returns an IO String, which because of my handwaving above, we can think of as a String in an IO context, or, an IO [Char]. I think?

Let’s see what the compiler tells us!

part01 :: IO ()
part01 = do
   raw <- readFile "data/Day00.txt"
   print (computeAnswer raw)

It runs! It terminates! And it’s the right answer! How about that?

Tomorrow: part 2 of Day 1.